use crate::types::mesh::MeshData;
use crate::types::response::{
CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
QuickMetadataEntitySummary, QuickMetadataSpatialNode,
};
use ifc_lite_core::{
build_entity_index, AttributeValue, DecodedEntity, EntityDecoder,
EntityIndex, EntityScanner, IfcType,
};
use ifc_lite_geometry::TessellationQuality;
use ifc_lite_geometry::{calculate_normals, GeometryRouter};
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OpeningFilterMode {
#[default]
Default = 0,
IgnoreAll = 1,
IgnoreOpaque = 2,
}
impl OpeningFilterMode {
pub fn cache_key_suffix(&self) -> &'static str {
match self {
Self::Default => "default",
Self::IgnoreAll => "ignore_all",
Self::IgnoreOpaque => "ignore_opaque",
}
}
}
pub struct ProcessingResult {
pub meshes: Vec<MeshData>,
pub mesh_coordinate_space: Option<String>,
pub site_transform: Option<Vec<f64>>,
pub building_transform: Option<Vec<f64>>,
pub metadata: ModelMetadata,
pub stats: ProcessingStats,
}
#[derive(Debug, Clone, Copy)]
pub struct StreamingOptions {
pub initial_batch_size: usize,
pub throughput_batch_size: usize,
pub fast_first_batch: bool,
pub include_properties: bool,
pub include_presentation_layers: bool,
pub emit_quick_metadata_bootstrap: bool,
pub retain_emitted_meshes: bool,
pub tessellation_quality: TessellationQuality,
}
impl Default for StreamingOptions {
fn default() -> Self {
Self {
initial_batch_size: 50,
throughput_batch_size: 50,
fast_first_batch: false,
include_properties: true,
include_presentation_layers: true,
emit_quick_metadata_bootstrap: false,
retain_emitted_meshes: true,
tessellation_quality: TessellationQuality::default(),
}
}
}
const SITE_LOCAL_MESH_COORDINATE_SPACE: &str = "site_local";
const MODEL_RTC_MESH_COORDINATE_SPACE: &str = "model_rtc";
const RAW_IFC_MESH_COORDINATE_SPACE: &str = "raw_ifc";
const PLACEMENT_IDENTITY_EPSILON: f64 = 1e-9;
#[inline]
fn translation_is_nonidentity(t: (f64, f64, f64)) -> bool {
t.0.abs() > PLACEMENT_IDENTITY_EPSILON
|| t.1.abs() > PLACEMENT_IDENTITY_EPSILON
|| t.2.abs() > PLACEMENT_IDENTITY_EPSILON
}
fn apply_inverse_rotation_in_place(values: &mut [f32], column_major_matrix: &[f64]) {
if values.len() < 3 || column_major_matrix.len() < 16 {
return;
}
let r00 = column_major_matrix[0];
let r10 = column_major_matrix[1];
let r20 = column_major_matrix[2];
let r01 = column_major_matrix[4];
let r11 = column_major_matrix[5];
let r21 = column_major_matrix[6];
let r02 = column_major_matrix[8];
let r12 = column_major_matrix[9];
let r22 = column_major_matrix[10];
let is_identity = (r00 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
&& r10.abs() < PLACEMENT_IDENTITY_EPSILON
&& r20.abs() < PLACEMENT_IDENTITY_EPSILON
&& r01.abs() < PLACEMENT_IDENTITY_EPSILON
&& (r11 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
&& r21.abs() < PLACEMENT_IDENTITY_EPSILON
&& r02.abs() < PLACEMENT_IDENTITY_EPSILON
&& r12.abs() < PLACEMENT_IDENTITY_EPSILON
&& (r22 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON;
if is_identity {
return;
}
for chunk in values.chunks_exact_mut(3) {
let x = chunk[0] as f64;
let y = chunk[1] as f64;
let z = chunk[2] as f64;
chunk[0] = (r00 * x + r10 * y + r20 * z) as f32;
chunk[1] = (r01 * x + r11 * y + r21 * z) as f32;
chunk[2] = (r02 * x + r12 * y + r22 * z) as f32;
}
}
pub fn convert_mesh_to_site_local(mesh: &mut MeshData, site_transform: Option<&Vec<f64>>) {
let Some(site_transform) = site_transform else {
return;
};
apply_inverse_rotation_in_place(&mut mesh.positions, site_transform);
apply_inverse_rotation_in_place(&mut mesh.normals, site_transform);
}
struct EntityJob {
id: u32,
ifc_type: IfcType,
start: usize,
end: usize,
product_definition_shape_id: Option<u32>,
element_color: [f32; 4],
global_id: Option<String>,
name: Option<String>,
presentation_layer: Option<String>,
space_zone_properties: Option<BTreeMap<String, String>>,
representation_map_id: Option<u32>,
}
fn populate_entity_job_metadata(
job: &mut EntityJob,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
element_material_color: &FxHashMap<u32, [f32; 4]>,
layer_by_assigned_representation: &FxHashMap<u32, String>,
color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
decoder: &mut EntityDecoder,
include_presentation_layers: bool,
) {
if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
return;
}
let Ok(entity) = decoder.decode_at(job.start, job.end) else {
return;
};
job.global_id = normalize_optional_string(entity.get_string(0));
job.name = normalize_optional_string(entity.get_string(2));
job.product_definition_shape_id = entity.get_ref(6);
let Some(product_definition_shape_id) = job.product_definition_shape_id else {
return;
};
let resolved_color = color_cache_by_product_definition_shape
.entry(product_definition_shape_id)
.or_insert_with(|| {
resolve_element_color_for_product_definition_shape(
product_definition_shape_id,
geometry_style_index,
decoder,
)
});
if let Some(color) = resolved_color {
job.element_color = *color;
} else if let Some(color) = element_material_color.get(&job.id) {
job.element_color = *color;
}
if include_presentation_layers {
let resolved_layer = layer_cache_by_product_definition_shape
.entry(product_definition_shape_id)
.or_insert_with(|| {
resolve_presentation_layer_for_product_definition_shape(
product_definition_shape_id,
layer_by_assigned_representation,
layer_cache_by_representation,
decoder,
)
});
job.presentation_layer = resolved_layer.clone();
}
}
#[derive(Debug, Clone)]
struct GeometryStyleInfo {
color: [f32; 4],
#[allow(dead_code)]
shading_color: Option<[f32; 4]>,
material_name: Option<String>,
}
#[derive(Debug, Clone)]
struct PropertySetDefinition {
name: Option<String>,
property_ids: Vec<u32>,
}
#[derive(Debug, Clone)]
struct RelDefinesByPropertiesLink {
property_set_id: u32,
related_object_ids: Vec<u32>,
}
fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
let list = entity.get_list(index)?;
let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
if refs.is_empty() {
None
} else {
Some(refs)
}
}
fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
let value = raw?.trim();
if value.is_empty() || value == "$" {
return None;
}
Some(value.to_string())
}
fn normalize_ifc_property_name(raw: Option<&str>) -> Option<String> {
let name = normalize_optional_string(raw)?;
let cleaned = name.trim();
if cleaned.is_empty() {
return None;
}
Some(cleaned.to_string())
}
fn is_space_or_zone_type(ifc_type: &IfcType) -> bool {
matches!(
ifc_type,
IfcType::IfcSpace
| IfcType::IfcSpaceType
| IfcType::IfcZone
| IfcType::IfcSpatialZone
| IfcType::IfcSpatialZoneType
)
}
fn collect_property_set_definition(property_set: &DecodedEntity) -> Option<PropertySetDefinition> {
let property_ids = property_set
.get_list(4)
.or_else(|| property_set.get_list(2))
.map(|items| {
items
.iter()
.filter_map(AttributeValue::as_entity_ref)
.collect::<Vec<u32>>()
})
.unwrap_or_default();
if property_ids.is_empty() {
return None;
}
let name = normalize_optional_string(property_set.get_string(2))
.or_else(|| normalize_optional_string(property_set.get_string(0)));
Some(PropertySetDefinition { name, property_ids })
}
fn collect_rel_defines_by_properties_link(
rel_defines: &DecodedEntity,
) -> Option<RelDefinesByPropertiesLink> {
let property_set_id = rel_defines.get_ref(5).or_else(|| rel_defines.get_ref(3))?;
let related_object_ids = rel_defines
.get_list(4)
.or_else(|| rel_defines.get_list(2))
.map(|items| {
items
.iter()
.filter_map(AttributeValue::as_entity_ref)
.collect::<Vec<u32>>()
})
.unwrap_or_default();
if related_object_ids.is_empty() {
return None;
}
Some(RelDefinesByPropertiesLink {
property_set_id,
related_object_ids,
})
}
fn attribute_list_to_string(values: &[AttributeValue]) -> Option<String> {
let tokens = values
.iter()
.filter_map(attribute_value_to_string)
.collect::<Vec<String>>();
if tokens.is_empty() {
return None;
}
Some(tokens.join("; "))
}
fn attribute_value_to_string(value: &AttributeValue) -> Option<String> {
match value {
AttributeValue::Null | AttributeValue::Derived => None,
AttributeValue::String(text) => normalize_optional_string(Some(text)),
AttributeValue::Enum(text) => normalize_optional_string(Some(text.trim_matches('.'))),
AttributeValue::Integer(number) => Some(number.to_string()),
AttributeValue::Float(number) => Some(number.to_string()),
AttributeValue::EntityRef(id) => Some(format!("#{id}")),
AttributeValue::List(values) => {
if values.len() >= 2 && matches!(values.first(), Some(AttributeValue::String(_))) {
return values.get(1).and_then(attribute_value_to_string);
}
attribute_list_to_string(values)
}
}
}
fn extract_property_name_and_value(property_entity: &DecodedEntity) -> Option<(String, String)> {
let property_name = normalize_ifc_property_name(property_entity.get_string(0))
.or_else(|| normalize_ifc_property_name(property_entity.get_string(2)))?;
let property_type = property_entity.ifc_type.name();
let value = match property_type {
"IfcPropertySingleValue" => property_entity.get(2).and_then(attribute_value_to_string),
"IfcPropertyEnumeratedValue" => property_entity.get(2).and_then(attribute_value_to_string),
"IfcPropertyListValue" => property_entity.get(2).and_then(attribute_value_to_string),
"IfcPropertyBoundedValue" => {
let lower = property_entity.get(2).and_then(attribute_value_to_string);
let upper = property_entity.get(3).and_then(attribute_value_to_string);
match (lower, upper) {
(Some(lo), Some(hi)) => Some(format!("{lo}..{hi}")),
(Some(lo), None) => Some(lo),
(None, Some(hi)) => Some(hi),
(None, None) => None,
}
}
"IfcPropertyReferenceValue" => property_entity.get(2).and_then(attribute_value_to_string),
_ => None,
}?;
let normalized_value = value.trim();
if normalized_value.is_empty() || normalized_value == "$" {
return None;
}
Some((property_name, normalized_value.to_string()))
}
fn add_space_zone_property(
attributes: &mut BTreeMap<String, String>,
property_set_name: Option<&str>,
property_name: &str,
property_value: &str,
) {
if property_name.trim().is_empty() || property_value.trim().is_empty() {
return;
}
attributes
.entry(property_name.to_string())
.or_insert_with(|| property_value.to_string());
if let Some(pset_name) = normalize_optional_string(property_set_name) {
let scoped_name = format!("{}.{}", pset_name, property_name);
attributes
.entry(scoped_name)
.or_insert_with(|| property_value.to_string());
}
}
fn build_space_zone_properties_by_entity(
entity_jobs: &[EntityJob],
property_values_by_id: &FxHashMap<u32, (String, String)>,
property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
rel_defines_by_properties: &[RelDefinesByPropertiesLink],
) -> FxHashMap<u32, BTreeMap<String, String>> {
let mut target_space_zone_ids = FxHashMap::default();
for job in entity_jobs
.iter()
.filter(|job| is_space_or_zone_type(&job.ifc_type))
{
target_space_zone_ids.insert(job.id, ());
}
if target_space_zone_ids.is_empty() {
return FxHashMap::default();
}
let mut properties_by_entity: FxHashMap<u32, BTreeMap<String, String>> = FxHashMap::default();
for link in rel_defines_by_properties {
let Some(property_set) = property_sets_by_id.get(&link.property_set_id) else {
continue;
};
for related_id in &link.related_object_ids {
if !target_space_zone_ids.contains_key(related_id) {
continue;
}
let attributes = properties_by_entity.entry(*related_id).or_default();
for property_id in &property_set.property_ids {
let Some((property_name, property_value)) = property_values_by_id.get(property_id)
else {
continue;
};
add_space_zone_property(
attributes,
property_set.name.as_deref(),
property_name,
property_value,
);
}
}
}
properties_by_entity
}
fn assign_space_zone_properties(
entity_jobs: &mut [EntityJob],
property_values_by_id: &FxHashMap<u32, (String, String)>,
property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
rel_defines_by_properties: &[RelDefinesByPropertiesLink],
) {
let properties_by_entity = build_space_zone_properties_by_entity(
entity_jobs,
property_values_by_id,
property_sets_by_id,
rel_defines_by_properties,
);
if properties_by_entity.is_empty() {
return;
}
for job in entity_jobs.iter_mut() {
if let Some(properties) = properties_by_entity.get(&job.id) {
job.space_zone_properties = Some(properties.clone());
}
}
}
#[derive(Clone)]
struct QuickSpatialNodeEntry {
express_id: u32,
type_name: String,
name: String,
elevation: Option<f64>,
children: Vec<u32>,
elements: Vec<u32>,
parent: Option<u32>,
}
#[inline]
fn is_quick_spatial_type_ci(type_name: &str) -> bool {
type_name.eq_ignore_ascii_case("IFCPROJECT")
|| type_name.eq_ignore_ascii_case("IFCSITE")
|| type_name.eq_ignore_ascii_case("IFCBUILDING")
|| type_name.eq_ignore_ascii_case("IFCBUILDINGSTOREY")
|| type_name.eq_ignore_ascii_case("IFCSPACE")
|| type_name.eq_ignore_ascii_case("IFCFACILITY")
|| type_name.eq_ignore_ascii_case("IFCFACILITYPART")
|| type_name.eq_ignore_ascii_case("IFCBRIDGE")
|| type_name.eq_ignore_ascii_case("IFCBRIDGEPART")
|| type_name.eq_ignore_ascii_case("IFCROAD")
|| type_name.eq_ignore_ascii_case("IFCROADPART")
|| type_name.eq_ignore_ascii_case("IFCRAILWAY")
|| type_name.eq_ignore_ascii_case("IFCRAILWAYPART")
}
fn parse_step_arguments(entity_bytes: &[u8]) -> Vec<&[u8]> {
let Some(open_idx) = entity_bytes.iter().position(|byte| *byte == b'(') else {
return Vec::new();
};
let Some(close_idx) = entity_bytes.iter().rposition(|byte| *byte == b')') else {
return Vec::new();
};
if close_idx <= open_idx {
return Vec::new();
}
let args = &entity_bytes[open_idx + 1..close_idx];
let mut parts = Vec::new();
let mut in_string = false;
let mut depth = 0i32;
let mut start = 0usize;
let bytes = args;
let mut index = 0usize;
while index < bytes.len() {
match bytes[index] {
b'\'' => {
if in_string && index + 1 < bytes.len() && bytes[index + 1] == b'\'' {
index += 1;
} else {
in_string = !in_string;
}
}
b'(' if !in_string => depth += 1,
b')' if !in_string => depth -= 1,
b',' if !in_string && depth == 0 => {
parts.push(args[start..index].trim_ascii());
start = index + 1;
}
_ => {}
}
index += 1;
}
if start <= args.len() {
parts.push(args[start..].trim_ascii());
}
parts
}
fn parse_step_string(token: &[u8]) -> Option<String> {
let trimmed = token.trim_ascii();
if trimmed.len() < 2 || trimmed[0] != b'\'' || trimmed[trimmed.len() - 1] != b'\'' {
return None;
}
Some(String::from_utf8_lossy(&trimmed[1..trimmed.len() - 1]).replace("''", "'"))
}
fn parse_step_ref(token: &[u8]) -> Option<u32> {
std::str::from_utf8(token.trim_ascii().strip_prefix(b"#")?)
.ok()?
.parse()
.ok()
}
fn parse_step_ref_list(token: &[u8]) -> Vec<u32> {
let trimmed = token.trim_ascii();
let inner = trimmed
.strip_prefix(b"(")
.and_then(|value| value.strip_suffix(b")"))
.unwrap_or(trimmed);
inner.split(|byte| *byte == b',').filter_map(parse_step_ref).collect()
}
fn extract_name_from_args(args: &[&[u8]], fallback: &str) -> String {
args.get(2)
.and_then(|token| parse_step_string(token))
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| fallback.to_string())
}
fn extract_storey_elevation_from_args(args: &[&[u8]]) -> Option<f64> {
for index in [9usize, 8usize] {
if let Some(value) = args
.get(index)
.and_then(|token| std::str::from_utf8(token.trim_ascii()).ok())
.and_then(|token| token.parse::<f64>().ok())
{
return Some(value);
}
}
args.iter()
.filter_map(|token| std::str::from_utf8(token.trim_ascii()).ok())
.filter_map(|token| token.parse::<f64>().ok())
.find(|value| value.abs() < 10_000.0)
}
fn build_quick_spatial_tree_node(
express_id: u32,
nodes: &HashMap<u32, QuickSpatialNodeEntry>,
element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
) -> Result<QuickMetadataSpatialNode, String> {
let node = nodes
.get(&express_id)
.ok_or_else(|| format!("Quick spatial node #{express_id} not found"))?;
let mut children = Vec::with_capacity(node.children.len());
for child_id in &node.children {
children.push(build_quick_spatial_tree_node(
*child_id,
nodes,
element_summaries,
)?);
}
let elements = node
.elements
.iter()
.map(|element_id| {
element_summaries
.get(element_id)
.cloned()
.unwrap_or(QuickMetadataEntitySummary {
express_id: *element_id,
type_name: "IfcProduct".to_string(),
name: format!("IfcProduct #{}", element_id),
global_id: None,
kind: "element".to_string(),
has_children: false,
element_count: None,
elevation: None,
})
})
.collect();
Ok(QuickMetadataSpatialNode {
summary: QuickMetadataEntitySummary {
express_id: node.express_id,
type_name: node.type_name.clone(),
name: node.name.clone(),
global_id: None,
kind: "spatial".to_string(),
has_children: !node.children.is_empty() || !node.elements.is_empty(),
element_count: Some(node.elements.len()),
elevation: node.elevation,
},
children,
elements,
})
}
fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
match ifc_type {
IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
IfcType::IfcSlab => 95,
IfcType::IfcColumn => 90,
IfcType::IfcBeam => 85,
IfcType::IfcRoof => 80,
IfcType::IfcStair | IfcType::IfcStairFlight => 75,
IfcType::IfcCurtainWall => 70,
IfcType::IfcFooting | IfcType::IfcPile => 65,
IfcType::IfcDoor | IfcType::IfcWindow => 30,
IfcType::IfcFurnishingElement => 10,
_ => 50,
}
}
pub fn process_geometry<T>(content: &T) -> ProcessingResult
where
T: AsRef<[u8]> + ?Sized,
{
process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
}
pub fn process_geometry_streaming(
content: &[u8],
batch_size: usize,
on_batch: impl FnMut(&[MeshData], usize, usize),
) -> ProcessingResult {
process_geometry_streaming_with_options(
content,
StreamingOptions {
initial_batch_size: batch_size,
throughput_batch_size: batch_size,
..StreamingOptions::default()
},
on_batch,
|_| {},
)
}
pub fn process_geometry_streaming_with_options(
content: &[u8],
options: StreamingOptions,
on_batch: impl FnMut(&[MeshData], usize, usize),
on_color_update: impl FnMut(&[(u32, [f32; 4])]),
) -> ProcessingResult {
process_geometry_streaming_with_options_and_bootstrap(
content,
options,
on_batch,
on_color_update,
|_| {},
)
}
pub fn process_geometry_streaming_with_options_and_bootstrap(
content: &[u8],
options: StreamingOptions,
on_batch: impl FnMut(&[MeshData], usize, usize),
on_color_update: impl FnMut(&[(u32, [f32; 4])]),
on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
) -> ProcessingResult {
process_geometry_streaming_filtered_with_options(
content,
OpeningFilterMode::Default,
options,
on_batch,
on_color_update,
on_quick_metadata_bootstrap,
)
}
pub fn process_geometry_filtered<T>(
content: &T,
opening_filter: OpeningFilterMode,
) -> ProcessingResult
where
T: AsRef<[u8]> + ?Sized,
{
process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
}
pub fn process_geometry_filtered_with_quality<T>(
content: &T,
opening_filter: OpeningFilterMode,
tessellation_quality: TessellationQuality,
) -> ProcessingResult
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
process_geometry_streaming_filtered_with_options(
content,
opening_filter,
StreamingOptions {
initial_batch_size: usize::MAX,
throughput_batch_size: usize::MAX,
tessellation_quality,
..StreamingOptions::default()
},
|_, _, _| {},
|_| {},
|_| {},
)
}
pub fn process_geometry_streaming_filtered(
content: &[u8],
opening_filter: OpeningFilterMode,
batch_size: usize,
on_batch: impl FnMut(&[MeshData], usize, usize),
on_color_update: impl FnMut(&[(u32, [f32; 4])]),
) -> ProcessingResult {
process_geometry_streaming_filtered_with_options(
content,
opening_filter,
StreamingOptions {
initial_batch_size: batch_size,
throughput_batch_size: batch_size,
..StreamingOptions::default()
},
on_batch,
on_color_update,
|_| {},
)
}
pub fn process_geometry_streaming_filtered_with_options(
content: &[u8],
opening_filter: OpeningFilterMode,
options: StreamingOptions,
mut on_batch: impl FnMut(&[MeshData], usize, usize),
mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
) -> ProcessingResult {
let total_start = std::time::Instant::now();
let parse_start = std::time::Instant::now();
let entity_scan_start = std::time::Instant::now();
tracing::info!(
content_size = content.len(),
"Starting IFC geometry processing"
);
let entity_index = Arc::new(build_entity_index(content));
let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
tracing::debug!("Built entity index");
let mut geometry_style_index: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
let mut indexed_colour_index: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
let mut indexed_colour_full: FxHashMap<u32, crate::style::FullIndexedColourMap> =
FxHashMap::default();
let mut orphan_styled_items: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
let mut material_def_reprs: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let mut element_to_material: FxHashMap<u32, u32> = FxHashMap::default();
let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
let mut property_values_by_id: FxHashMap<u32, (String, String)> = FxHashMap::default();
let mut property_sets_by_id: FxHashMap<u32, PropertySetDefinition> = FxHashMap::default();
let mut rel_defines_by_properties: Vec<RelDefinesByPropertiesLink> = Vec::new();
let mut scanner = EntityScanner::new(content);
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let mut filling_by_opening: FxHashMap<u32, u32> = FxHashMap::default();
let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
let mut quick_spatial_nodes =
quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
let mut quick_aggregate_links = if quick_metadata_enabled {
Vec::<(u32, Vec<u32>)>::new()
} else {
Vec::new()
};
let mut quick_containment_links = if quick_metadata_enabled {
Vec::<(u32, Vec<u32>)>::new()
} else {
Vec::new()
};
let mut quick_element_summaries = if quick_metadata_enabled {
HashMap::<u32, QuickMetadataEntitySummary>::new()
} else {
HashMap::new()
};
let mut schema_version = "IFC2X3".to_string();
let mut total_entities = 0usize;
let mut site_entity_pos: Option<(usize, usize)> = None;
let mut building_entity_pos: Option<(usize, usize)> = None;
let defer_style_updates = options.fast_first_batch
&& opening_filter == OpeningFilterMode::Default
&& !options.include_presentation_layers;
let mut deferred_styled_item_positions: Vec<(usize, usize)> = Vec::new();
while let Some((id, type_name, start, end)) = scanner.next_entity() {
total_entities += 1;
if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
if is_quick_spatial_type_ci(type_name) {
let args = parse_step_arguments(&content[start..end]);
let fallback = format!("{type_name} #{id}");
spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
express_id: id,
type_name: type_name.to_string(),
name: extract_name_from_args(&args, &fallback),
elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
extract_storey_elevation_from_args(&args)
} else {
None
},
children: Vec::new(),
elements: Vec::new(),
parent: None,
});
} else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
let args = parse_step_arguments(&content[start..end]);
if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
quick_aggregate_links.push((
parent_id,
args.get(5)
.map(|token| parse_step_ref_list(token))
.unwrap_or_default(),
));
}
} else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE")
|| type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE")
{
let args = parse_step_arguments(&content[start..end]);
if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
quick_containment_links.push((
parent_id,
args.get(4)
.map(|token| parse_step_ref_list(token))
.unwrap_or_default(),
));
}
}
}
if type_name == "IFCINDEXEDCOLOURMAP" {
if let Ok(icm) = decoder.decode_at(start, end) {
if let Some(full) =
crate::style::resolve_indexed_colour_map_full(&icm, &mut decoder)
{
let geometry_id = full.geometry_id;
indexed_colour_index
.entry(geometry_id)
.or_insert(full.dominant().to_array());
indexed_colour_full.entry(geometry_id).or_insert(full);
}
}
continue;
}
if type_name == "IFCSTYLEDITEM" {
if defer_style_updates {
if let Ok(styled_item) = decoder.decode_at(start, end) {
if styled_item.get_ref(0).is_none() {
if let Some(info) =
extract_style_info_from_styled_item(&styled_item, &mut decoder)
{
orphan_styled_items.insert(id, info.color);
}
continue;
}
}
deferred_styled_item_positions.push((start, end));
continue;
}
if let Ok(styled_item) = decoder.decode_at(start, end) {
if styled_item.get_ref(0).is_none() {
if let Some(info) =
extract_style_info_from_styled_item(&styled_item, &mut decoder)
{
orphan_styled_items.insert(id, info.color);
}
} else {
collect_geometry_style_info(
&mut geometry_style_index,
&styled_item,
&mut decoder,
);
}
}
continue;
} else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
if let Ok(entity) = decoder.decode_at(start, end) {
if let Some(material_id) = entity.get_ref(3) {
if let Some(reprs) = get_refs_from_list(&entity, 2) {
material_def_reprs
.entry(material_id)
.or_default()
.extend(reprs);
}
}
}
continue;
} else if type_name == "IFCRELASSOCIATESMATERIAL" {
if let Ok(entity) = decoder.decode_at(start, end) {
if let Some(material_select_id) = entity.get_ref(5) {
if let Some(related) = get_refs_from_list(&entity, 4) {
for element_id in related {
element_to_material.insert(element_id, material_select_id);
}
}
}
}
continue;
} else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
if !options.include_presentation_layers {
continue;
}
if let Ok(layer_assignment) = decoder.decode_at(start, end) {
collect_presentation_layer_assignments(
&mut presentation_layer_by_assigned_id,
&layer_assignment,
);
}
continue;
} else if type_name == "IFCPROPERTYSET" {
if !options.include_properties {
continue;
}
if let Ok(property_set) = decoder.decode_at(start, end) {
if let Some(definition) = collect_property_set_definition(&property_set) {
property_sets_by_id.insert(id, definition);
}
}
continue;
} else if type_name == "IFCRELDEFINESBYPROPERTIES" {
if !options.include_properties {
continue;
}
if let Ok(rel_defines) = decoder.decode_at(start, end) {
if let Some(link) = collect_rel_defines_by_properties_link(&rel_defines) {
rel_defines_by_properties.push(link);
}
}
continue;
} else if type_name.starts_with("IFCPROPERTY") {
if !options.include_properties {
continue;
}
if let Ok(property_entity) = decoder.decode_at(start, end) {
if let Some((name, value)) = extract_property_name_and_value(&property_entity) {
property_values_by_id.insert(id, (name, value));
}
}
continue;
} else if type_name == "IFCRELVOIDSELEMENT" {
if let Ok(entity) = decoder.decode_at(start, end) {
if let (Some(host), Some(opening)) = (entity.get_ref(4), entity.get_ref(5)) {
void_index.entry(host).or_default().push(opening);
}
}
} else if type_name == "IFCRELFILLSELEMENT" {
if let Ok(entity) = decoder.decode_at(start, end) {
if let (Some(opening_id), Some(filling_id)) = (entity.get_ref(4), entity.get_ref(5))
{
filling_by_opening.insert(opening_id, filling_id);
}
}
} else if type_name == "IFCRELAGGREGATES" {
let args = parse_step_arguments(&content[start..end]);
if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
let kids = args
.get(5)
.map(|token| parse_step_ref_list(token))
.unwrap_or_default();
if !kids.is_empty() {
aggregate_children
.entry(parent_id)
.or_default()
.extend(kids);
}
}
} else if type_name == "IFCSITE" && site_entity_pos.is_none() {
site_entity_pos = Some((start, end));
} else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
building_entity_pos = Some((start, end));
}
if ifc_lite_core::has_geometry_by_name(type_name) {
let ifc_type = IfcType::from_str(type_name);
if quick_metadata_enabled {
quick_element_summaries.insert(
id,
QuickMetadataEntitySummary {
express_id: id,
type_name: type_name.to_string(),
name: format!("{type_name} #{id}"),
global_id: None,
kind: "element".to_string(),
has_children: false,
element_count: None,
elevation: None,
},
);
}
entity_jobs.push(EntityJob {
id,
ifc_type: ifc_type.clone(),
start,
end,
product_definition_shape_id: None,
element_color: crate::style::default_color_for_type(ifc_type).to_array(),
global_id: None,
name: None,
presentation_layer: None,
space_zone_properties: None,
representation_map_id: None,
});
}
else if type_name == "IFCMAPPEDITEM" {
let args = parse_step_arguments(&content[start..end]);
if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
referenced_representation_maps.insert(source_id);
}
} else if type_name == "IFCRELDEFINESBYTYPE" {
let args = parse_step_arguments(&content[start..end]);
if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
instantiated_type_ids.insert(type_id);
}
} else if (type_name.ends_with("TYPE") || type_name.ends_with("STYLE"))
&& IfcType::from_str(type_name).is_subtype_of(IfcType::IfcTypeProduct)
{
let args = parse_step_arguments(&content[start..end]);
let rep_map_ids = args
.get(6)
.map(|token| parse_step_ref_list(token))
.unwrap_or_default();
if !rep_map_ids.is_empty() {
type_product_geometry.push((
id,
start,
end,
IfcType::from_str(type_name),
rep_map_ids,
));
}
}
}
for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
if instantiated_type_ids.contains(type_id) {
continue;
}
for &rep_map_id in rep_map_ids {
if referenced_representation_maps.contains(&rep_map_id) {
continue;
}
entity_jobs.push(EntityJob {
id: *type_id,
ifc_type: ifc_type.clone(),
start: *start,
end: *end,
product_definition_shape_id: None,
element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
global_id: None,
name: None,
presentation_layer: None,
space_zone_properties: None,
representation_map_id: Some(rep_map_id),
});
}
}
ifc_lite_geometry::propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
let entity_scan_time = entity_scan_start.elapsed();
let lookup_start = std::time::Instant::now();
if options.include_properties {
assign_space_zone_properties(
&mut entity_jobs,
&property_values_by_id,
&property_sets_by_id,
&rel_defines_by_properties,
);
}
if options.fast_first_batch {
entity_jobs.sort_by(|left, right| {
geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
});
}
let lookup_time = lookup_start.elapsed();
let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
&entity_jobs,
&void_index,
&filling_by_opening,
&geometry_style_index,
&mut decoder,
opening_filter,
);
if content
.windows(b"IFC4X3".len())
.any(|window| window == b"IFC4X3")
{
schema_version = "IFC4X3".into();
} else if content
.windows(b"IFC4".len())
.any(|window| window == b"IFC4")
{
schema_version = "IFC4".into();
}
let geometry_entity_count = entity_jobs.len();
tracing::info!(
total_entities = total_entities,
geometry_entities = geometry_entity_count,
voids = void_index.len(),
schema_version = %schema_version,
"Entity scanning complete"
);
if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
for (parent_id, child_ids) in quick_aggregate_links {
if !spatial_nodes.contains_key(&parent_id) {
continue;
}
for child_id in child_ids {
if !spatial_nodes.contains_key(&child_id) {
continue;
}
if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
parent.children.push(child_id);
}
if let Some(child) = spatial_nodes.get_mut(&child_id) {
child.parent = Some(parent_id);
}
}
}
for (parent_id, element_ids) in quick_containment_links {
if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
parent.elements.extend(element_ids);
}
}
let mut root_id = spatial_nodes
.values()
.find(|node| node.type_name == "IfcProject")
.map(|node| node.express_id);
if root_id.is_none() {
root_id = spatial_nodes
.values()
.find(|node| node.parent.is_none())
.map(|node| node.express_id);
}
let spatial_tree = root_id
.map(|root| {
build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
})
.transpose()
.unwrap_or(None);
on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
schema_version: schema_version.clone(),
entity_count: total_entities,
spatial_tree,
});
}
let preprocess_start = std::time::Instant::now();
let mut router = GeometryRouter::with_units(content, &mut decoder);
router.set_tessellation_quality(options.tessellation_quality);
let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
let entity = decoder.decode_at(start, end).ok()?;
let matrix = router
.resolve_scaled_placement(&entity, &mut decoder)
.ok()?;
Some(matrix.to_vec())
});
let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
let entity = decoder.decode_at(start, end).ok()?;
let matrix = router
.resolve_scaled_placement(&entity, &mut decoder)
.ok()?;
Some(matrix.to_vec())
});
let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
.iter()
.map(|job| (job.id, job.start, job.end, job.ifc_type))
.collect();
let detected_rtc_offset =
router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
let site_rtc = site_transform
.as_ref()
.map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
(site, SITE_LOCAL_MESH_COORDINATE_SPACE)
} else if detected_has_offset {
(detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
} else {
((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
};
let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
router.set_rtc_offset(rtc_offset);
let preprocess_time = preprocess_start.elapsed();
let parse_time = parse_start.elapsed();
tracing::info!(
entity_scan_time_ms = entity_scan_time.as_millis(),
lookup_time_ms = lookup_time.as_millis(),
preprocess_time_ms = preprocess_time.as_millis(),
parse_time_ms = parse_time.as_millis(),
"Parse phase complete, starting geometry extraction"
);
let geometry_start = std::time::Instant::now();
let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
let rtc_offset = router.rtc_offset();
let void_index_arc = Arc::new(filtered_void_index);
let skipped_entity_ids = Arc::new(skipped_entity_ids);
merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
let mut geometry_style_index = Arc::new(geometry_style_index);
let indexed_colour_full = Arc::new(indexed_colour_full);
let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
content,
&mut decoder,
));
let element_material_colors = crate::style::build_element_material_colors(
&material_def_reprs,
&orphan_styled_items,
&element_to_material,
&mut decoder,
);
let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
.iter()
.filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
.collect();
let element_material_colors = Arc::new(element_material_colors);
let total_jobs = entity_jobs.len();
let initial_chunk_size = options.initial_batch_size.max(1);
let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
FxHashMap::default();
let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
FxHashMap::default();
let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
let mut meshes: Vec<MeshData> = Vec::new();
let mut processed_jobs = 0usize;
let mut total_meshes = 0usize;
let mut total_vertices = 0usize;
let mut total_triangles = 0usize;
let mut chunk_start = 0usize;
let mut current_chunk_size = initial_chunk_size;
let mut deferred_styles_applied = !defer_style_updates;
let csg_failure_collector: std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>> =
std::sync::Mutex::new(FxHashMap::default());
while chunk_start < total_jobs {
let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
#[cfg(not(target_arch = "wasm32"))]
{
let entity_index_for_meta = entity_index_arc.clone();
jobs_chunk.par_iter_mut().for_each(|job| {
if job.global_id.is_some()
|| job.name.is_some()
|| job.product_definition_shape_id.is_some()
{
return;
}
let mut local_decoder =
EntityDecoder::with_arc_index(content, entity_index_for_meta.clone());
let Ok(entity) = local_decoder.decode_at(job.start, job.end) else {
return;
};
job.global_id = normalize_optional_string(entity.get_string(0));
job.name = normalize_optional_string(entity.get_string(2));
job.product_definition_shape_id = entity.get_ref(6);
});
for job in jobs_chunk.iter_mut() {
let Some(pds_id) = job.product_definition_shape_id else {
continue;
};
let resolved_color = color_cache_by_product_definition_shape
.entry(pds_id)
.or_insert_with(|| {
resolve_element_color_for_product_definition_shape(
pds_id,
&geometry_style_index,
&mut decoder,
)
});
if let Some(color) = resolved_color {
job.element_color = *color;
} else if let Some(color) = element_material_color.get(&job.id) {
job.element_color = *color;
}
if options.include_presentation_layers {
let resolved_layer = layer_cache_by_product_definition_shape
.entry(pds_id)
.or_insert_with(|| {
resolve_presentation_layer_for_product_definition_shape(
pds_id,
&presentation_layer_by_assigned_id,
&mut layer_cache_by_representation,
&mut decoder,
)
});
job.presentation_layer = resolved_layer.clone();
}
}
}
#[cfg(target_arch = "wasm32")]
for job in jobs_chunk.iter_mut() {
populate_entity_job_metadata(
job,
&geometry_style_index,
&element_material_color,
&presentation_layer_by_assigned_id,
&mut color_cache_by_product_definition_shape,
&mut layer_cache_by_product_definition_shape,
&mut layer_cache_by_representation,
&mut decoder,
options.include_presentation_layers,
);
}
let site_local_rotation: Option<&Vec<f64>> =
if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
site_transform.as_ref()
} else {
None
};
let chunk_meshes: Vec<MeshData> = jobs_chunk
.par_iter()
.flat_map_iter(|job| {
process_entity_job(
job,
content,
&entity_index_arc,
unit_scale,
rtc_offset,
options.tessellation_quality,
void_index_arc.as_ref(),
skipped_entity_ids.as_ref(),
geometry_style_index.as_ref(),
indexed_colour_full.as_ref(),
element_material_colors.as_ref(),
texture_index.as_ref(),
site_local_rotation,
&csg_failure_collector,
)
})
.collect();
processed_jobs += jobs_chunk.len();
total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
total_triangles += chunk_meshes
.iter()
.map(|m| m.triangle_count())
.sum::<usize>();
if !chunk_meshes.is_empty() {
total_meshes += chunk_meshes.len();
let emit_mesh_chunk_size = current_chunk_size.max(1);
for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
on_batch(emitted_meshes, processed_jobs, total_jobs);
}
if options.retain_emitted_meshes {
meshes.extend(chunk_meshes);
}
if !deferred_styles_applied {
let mut rebuilt_styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
{
let mut style_decoder =
EntityDecoder::with_arc_index(content, entity_index_arc.clone());
for &(start, end) in &deferred_styled_item_positions {
if let Ok(styled_item) = style_decoder.decode_at(start, end) {
collect_geometry_style_info(
&mut rebuilt_styles,
&styled_item,
&mut style_decoder,
);
}
}
}
merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
geometry_style_index = Arc::new(rebuilt_styles);
let deferred_color_updates = build_color_updates_for_jobs(
&entity_jobs[..processed_jobs],
geometry_style_index.as_ref(),
content,
&entity_index_arc,
);
if !deferred_color_updates.is_empty() {
on_color_update(&deferred_color_updates);
}
deferred_styles_applied = true;
}
}
chunk_start = chunk_end;
current_chunk_size = throughput_chunk_size;
}
let geometry_time = geometry_start.elapsed();
let csg_failures = csg_failure_collector
.into_inner()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
let products_with_failures = csg_failures.len();
if total_csg_failures > 0 {
let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
for fails in csg_failures.values() {
for f in fails {
*by_reason.entry(f.reason.label()).or_insert(0) += 1;
}
}
let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
breakdown.sort_by(|a, b| b.1.cmp(&a.1));
let breakdown = breakdown
.iter()
.map(|(reason, count)| format!("{reason}={count}"))
.collect::<Vec<_>>()
.join(" ");
tracing::warn!(
total_csg_failures,
products_with_failures,
%breakdown,
"CSG failures during geometry extraction (cut dropped, host kept uncut)"
);
}
let total_time = total_start.elapsed();
tracing::info!(
meshes = meshes.len(),
vertices = total_vertices,
triangles = total_triangles,
geometry_time_ms = geometry_time.as_millis(),
total_time_ms = total_time.as_millis(),
"Geometry processing complete"
);
ProcessingResult {
meshes,
mesh_coordinate_space: Some(coord_space.to_string()),
site_transform,
building_transform,
metadata: ModelMetadata {
schema_version,
entity_count: total_entities,
geometry_entity_count,
coordinate_info: CoordinateInfo {
origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
is_geo_referenced: has_rtc_offset,
},
length_unit_scale: Some(unit_scale),
georeferencing: crate::extract_georeferencing(content),
},
stats: ProcessingStats {
total_meshes,
total_vertices,
total_triangles,
parse_time_ms: parse_time.as_millis() as u64,
entity_scan_time_ms: entity_scan_time.as_millis() as u64,
lookup_time_ms: lookup_time.as_millis() as u64,
preprocess_time_ms: preprocess_time.as_millis() as u64,
geometry_time_ms: geometry_time.as_millis() as u64,
total_time_ms: total_time.as_millis() as u64,
from_cache: false,
total_csg_failures: total_csg_failures as u64,
products_with_failures: products_with_failures as u64,
},
}
}
fn process_entity_job(
job: &EntityJob,
content: &[u8],
entity_index_arc: &Arc<EntityIndex>,
unit_scale: f64,
rtc_offset: (f64, f64, f64),
tessellation_quality: TessellationQuality,
void_index: &FxHashMap<u32, Vec<u32>>,
skipped_entity_ids: &HashSet<u32>,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
indexed_colour_full: &FxHashMap<u32, crate::style::FullIndexedColourMap>,
element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
texture_index: &FxHashMap<u32, ifc_lite_geometry::ResolvedTextureMap>,
site_local_rotation: Option<&Vec<f64>>,
csg_failure_collector: &std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>>,
) -> Vec<MeshData> {
if skipped_entity_ids.contains(&job.id) {
return Vec::new();
}
let mut local_decoder = EntityDecoder::with_arc_index(content, entity_index_arc.clone());
let entity = match local_decoder.decode_at(job.start, job.end) {
Ok(entity) => entity,
Err(_) => return Vec::new(),
};
let has_representation = entity.get(6).is_some_and(|a| !a.is_null());
if !has_representation {
return Vec::new();
}
let mut local_router = GeometryRouter::with_scale_and_quality(unit_scale, tessellation_quality);
local_router.set_rtc_offset(rtc_offset);
let local_router = local_router;
let result = (|| -> Vec<MeshData> {
let global_id = job.global_id.clone();
let name = job.name.clone();
let presentation_layer = job.presentation_layer.clone();
let space_zone_properties = job.space_zone_properties.clone();
let element_color = job.element_color;
if let Some(rep_map_id) = job.representation_map_id {
return process_type_representation_map_job(
job,
rep_map_id,
&local_router,
&mut local_decoder,
geometry_style_index,
texture_index,
element_color,
global_id,
name,
presentation_layer,
site_local_rotation,
);
}
let has_openings = void_index.get(&job.id).is_some_and(|v| !v.is_empty());
let mut emit_sub_meshes = |sub_meshes: ifc_lite_geometry::SubMeshCollection,
local_decoder: &mut EntityDecoder|
-> Vec<MeshData> {
let mut out: Vec<MeshData> = Vec::with_capacity(sub_meshes.len());
let material_colors = element_material_colors.get(&job.id);
let mut mat_color_idx = 0usize;
for sub in sub_meshes.sub_meshes {
let mut sub_mesh = sub.mesh;
if sub_mesh.is_empty() {
continue;
}
if sub_mesh.normals.is_empty() {
calculate_normals(&mut sub_mesh);
}
let style = geometry_style_index.get(&sub.geometry_id);
let direct_color = style.map(|s| s.color).or_else(|| {
find_geometry_item_color(sub.geometry_id, geometry_style_index, local_decoder)
});
let color = crate::style::resolve_submesh_color(
direct_color,
material_colors.map(|v| v.as_slice()),
&mut mat_color_idx,
element_color,
);
let material_name = style
.and_then(|s| s.material_name.as_ref())
.map(ToString::to_string);
let material_name = material_name.or_else(|| {
infer_opening_subpart_material_name(&job.ifc_type, color, sub.geometry_id)
});
let mut mesh_data = MeshData::new(
job.id,
job.ifc_type.name().to_string(),
sub_mesh.positions,
sub_mesh.normals,
sub_mesh.indices,
color,
)
.with_element_metadata(global_id.clone(), name.clone(), presentation_layer.clone())
.with_properties(space_zone_properties.clone())
.with_style_metadata(material_name, Some(sub.geometry_id));
convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
out.push(mesh_data);
}
out
};
if has_openings {
if let Ok(sub_meshes) = local_router.process_element_with_submeshes_and_voids(
&entity,
&mut local_decoder,
void_index,
) {
if !sub_meshes.is_empty() {
let out = emit_sub_meshes(sub_meshes, &mut local_decoder);
if !out.is_empty() {
return out;
}
}
}
} else {
let has_indexed_colour = !indexed_colour_full.is_empty()
&& find_indexed_colour_for_element(&entity, indexed_colour_full, &mut local_decoder)
.is_some();
if !has_indexed_colour {
if let Ok(sub_meshes) =
local_router.process_element_with_submeshes(&entity, &mut local_decoder)
{
if !sub_meshes.is_empty() {
let out = emit_sub_meshes(sub_meshes, &mut local_decoder);
if !out.is_empty() {
return out;
}
}
}
}
}
let _ = local_router.take_csg_failures();
let mut mesh_candidate = local_router
.process_element_with_voids(&entity, &mut local_decoder, void_index)
.ok();
let needs_fallback = match mesh_candidate.as_ref() {
Some(mesh) => mesh.is_empty(),
None => true,
};
if needs_fallback {
mesh_candidate = local_router
.process_element(&entity, &mut local_decoder)
.ok();
}
if let Some(mut mesh) = mesh_candidate {
if !mesh.is_empty() {
if !indexed_colour_full.is_empty() {
if let Some(full) = find_indexed_colour_for_element(
&entity,
indexed_colour_full,
&mut local_decoder,
) {
let geometry_id = full.geometry_id;
if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&mesh, full) {
let mut out: Vec<MeshData> = Vec::with_capacity(groups.len());
for (color, mut part) in groups {
if part.normals.is_empty() {
calculate_normals(&mut part);
}
let mut mesh_data = MeshData::new(
job.id,
job.ifc_type.name().to_string(),
part.positions,
part.normals,
part.indices,
color.to_array(),
)
.with_element_metadata(
global_id.clone(),
name.clone(),
presentation_layer.clone(),
)
.with_properties(space_zone_properties.clone())
.with_style_metadata(None, Some(geometry_id));
convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
out.push(mesh_data);
}
if !out.is_empty() {
return out;
}
}
}
}
if mesh.normals.is_empty() {
calculate_normals(&mut mesh);
}
let mut mesh_data = MeshData::new(
job.id,
job.ifc_type.name().to_string(),
mesh.positions,
mesh.normals,
mesh.indices,
element_color,
)
.with_element_metadata(global_id, name, presentation_layer)
.with_properties(space_zone_properties);
convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
return vec![mesh_data];
}
}
Vec::new()
})();
let failures = local_router.take_csg_failures();
if !failures.is_empty() {
if let Ok(mut collector) = csg_failure_collector.lock() {
for (product_id, fails) in failures {
collector.entry(product_id).or_default().extend(fails);
}
}
}
result
}
#[allow(clippy::too_many_arguments)]
fn process_type_representation_map_job(
job: &EntityJob,
rep_map_id: u32,
router: &GeometryRouter,
decoder: &mut EntityDecoder,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
texture_index: &FxHashMap<u32, ifc_lite_geometry::ResolvedTextureMap>,
element_color: [f32; 4],
global_id: Option<String>,
name: Option<String>,
presentation_layer: Option<String>,
site_local_rotation: Option<&Vec<f64>>,
) -> Vec<MeshData> {
let Ok(rep_map) = decoder.decode_by_id(rep_map_id) else {
return Vec::new();
};
let Ok(parts) =
router.process_representation_map_with_texture(&rep_map, decoder, texture_index)
else {
return Vec::new();
};
if parts.is_empty() {
return Vec::new();
}
let color = resolve_color_for_representation_map(rep_map_id, geometry_style_index, decoder)
.unwrap_or(element_color);
let mut out: Vec<MeshData> = Vec::with_capacity(parts.len());
for (mut mesh, uvs, texture) in parts {
if mesh.is_empty() {
continue;
}
if mesh.normals.is_empty() {
calculate_normals(&mut mesh);
}
let mut mesh_data = MeshData::new(
job.id,
job.ifc_type.name().to_string(),
mesh.positions,
mesh.normals,
mesh.indices,
color,
)
.with_element_metadata(global_id.clone(), name.clone(), presentation_layer.clone());
if let Some(tex) = texture {
mesh_data = mesh_data.with_texture(
uvs,
crate::types::mesh::MeshTextureData {
rgba: tex.rgba,
width: tex.width,
height: tex.height,
repeat_s: tex.repeat_s,
repeat_t: tex.repeat_t,
},
);
}
convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
out.push(mesh_data);
}
out
}
fn resolve_color_for_representation_map(
rep_map_id: u32,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
let rep_map = decoder.decode_by_id(rep_map_id).ok()?;
let mapped_rep_id = rep_map.get_ref(1)?;
let mapped_rep = decoder.decode_by_id(mapped_rep_id).ok()?;
let item_ids = get_refs_from_list(&mapped_rep, 3)?;
for item_id in item_ids {
if let Some(style) = geometry_style_index.get(&item_id) {
return Some(style.color);
}
if let Some(color) = find_geometry_item_color(item_id, geometry_style_index, decoder) {
return Some(color);
}
}
None
}
fn find_indexed_colour_for_element<'a>(
entity: &DecodedEntity,
indexed_colour_full: &'a FxHashMap<u32, crate::style::FullIndexedColourMap>,
decoder: &mut EntityDecoder,
) -> Option<&'a crate::style::FullIndexedColourMap> {
let pds_id = entity.get_ref(6)?;
let pds = decoder.decode_by_id(pds_id).ok()?;
let repr_ids = get_refs_from_list(&pds, 2)?;
for repr_id in repr_ids {
if let Ok(repr) = decoder.decode_by_id(repr_id) {
if let Some(items) = get_refs_from_list(&repr, 3) {
for item_id in items {
if let Some(full) = indexed_colour_full.get(&item_id) {
return Some(full);
}
}
}
}
}
None
}
fn merge_indexed_colours(
geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
indexed_colours: &FxHashMap<u32, [f32; 4]>,
) {
for (&geometry_id, &color) in indexed_colours {
geometry_styles
.entry(geometry_id)
.or_insert_with(|| GeometryStyleInfo {
color,
shading_color: None,
material_name: None,
});
}
}
fn collect_geometry_style_info(
geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
styled_item: &DecodedEntity,
decoder: &mut EntityDecoder,
) {
let Some(geometry_id) = styled_item.get_ref(0) else {
return;
};
if geometry_styles.contains_key(&geometry_id) {
return;
}
if let Some(style_info) = extract_style_info_from_styled_item(styled_item, decoder) {
geometry_styles.insert(geometry_id, style_info);
}
}
fn build_color_updates_for_jobs(
jobs: &[EntityJob],
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
content: &[u8],
entity_index: &Arc<EntityIndex>,
) -> Vec<(u32, [f32; 4])> {
let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
let mut updates: Vec<(u32, [f32; 4])> = Vec::new();
for job in jobs {
if let Some(rep_map_id) = job.representation_map_id {
if let Some(color) =
resolve_color_for_representation_map(rep_map_id, geometry_styles, &mut decoder)
{
if color != job.element_color {
updates.push((job.id, color));
}
}
continue;
}
let Ok(entity) = decoder.decode_at(job.start, job.end) else {
continue;
};
let Some(product_definition_shape_id) = entity.get_ref(6) else {
continue;
};
let Some(color) = resolve_element_color_for_product_definition_shape(
product_definition_shape_id,
geometry_styles,
&mut decoder,
) else {
continue;
};
if color != job.element_color {
updates.push((job.id, color));
}
}
updates
}
fn collect_presentation_layer_assignments(
layer_by_assigned_representation: &mut FxHashMap<u32, String>,
layer_assignment: &DecodedEntity,
) {
let Some(layer_name) = normalize_optional_string(layer_assignment.get_string(0)) else {
return;
};
let Some(assigned_items) = get_refs_from_list(layer_assignment, 2) else {
return;
};
for assigned in assigned_items {
layer_by_assigned_representation
.entry(assigned)
.or_insert_with(|| layer_name.clone());
}
}
fn resolve_element_color_for_product_definition_shape(
product_definition_shape_id: u32,
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
find_color_in_representation(product_definition_shape_id, geometry_styles, decoder)
}
fn resolve_presentation_layer_for_product_definition_shape(
product_definition_shape_id: u32,
layer_by_assigned_representation: &FxHashMap<u32, String>,
cache_by_representation: &mut FxHashMap<u32, Option<String>>,
decoder: &mut EntityDecoder,
) -> Option<String> {
if let Some(layer_name) = layer_by_assigned_representation.get(&product_definition_shape_id) {
return Some(layer_name.clone());
}
let product_definition_shape = decoder.decode_by_id(product_definition_shape_id).ok()?;
let representation_ids = get_refs_from_list(&product_definition_shape, 2)?;
for representation_id in representation_ids {
if let Some(layer_name) = resolve_presentation_layer_name(
representation_id,
layer_by_assigned_representation,
cache_by_representation,
decoder,
&mut Vec::new(),
) {
return Some(layer_name);
}
}
None
}
fn resolve_presentation_layer_name(
representation_id: u32,
layer_by_assigned_representation: &FxHashMap<u32, String>,
cache_by_representation: &mut FxHashMap<u32, Option<String>>,
decoder: &mut EntityDecoder,
traversal_stack: &mut Vec<u32>,
) -> Option<String> {
if let Some(cached) = cache_by_representation.get(&representation_id) {
return cached.clone();
}
if traversal_stack.contains(&representation_id) {
return None;
}
traversal_stack.push(representation_id);
if let Some(layer_name) = layer_by_assigned_representation.get(&representation_id) {
let result = Some(layer_name.clone());
cache_by_representation.insert(representation_id, result.clone());
traversal_stack.pop();
return result;
}
let mut resolved: Option<String> = None;
if let Ok(representation) = decoder.decode_by_id(representation_id) {
if let Some(items) = get_refs_from_list(&representation, 3) {
for item_id in items {
if let Some(layer_name) = layer_by_assigned_representation.get(&item_id) {
resolved = Some(layer_name.clone());
break;
}
if let Ok(item) = decoder.decode_by_id(item_id) {
if item.ifc_type == IfcType::IfcMappedItem {
if let Some(mapping_source_id) = item.get_ref(0) {
if let Ok(mapping_source) = decoder.decode_by_id(mapping_source_id) {
if let Some(mapped_representation_id) = mapping_source.get_ref(1) {
if let Some(layer_name) = resolve_presentation_layer_name(
mapped_representation_id,
layer_by_assigned_representation,
cache_by_representation,
decoder,
traversal_stack,
) {
resolved = Some(layer_name);
break;
}
}
}
}
}
}
}
}
}
traversal_stack.pop();
cache_by_representation.insert(representation_id, resolved.clone());
resolved
}
fn find_color_in_representation(
repr_id: u32,
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
let repr = decoder.decode_by_id(repr_id).ok()?;
let repr_list = get_refs_from_list(&repr, 2)?;
for shape_repr_id in repr_list {
if let Ok(shape_repr) = decoder.decode_by_id(shape_repr_id) {
if let Some(items) = get_refs_from_list(&shape_repr, 3) {
for item_id in items {
if let Some(style) = geometry_styles.get(&item_id) {
return Some(style.color);
}
if let Ok(item) = decoder.decode_by_id(item_id) {
if item.ifc_type == IfcType::IfcMappedItem {
if let Some(source_id) = item.get_ref(0) {
if let Ok(source) = decoder.decode_by_id(source_id) {
if let Some(mapped_repr_id) = source.get_ref(1) {
if let Some(color) = find_color_in_shape_representation(
mapped_repr_id,
geometry_styles,
decoder,
) {
return Some(color);
}
}
}
}
}
}
}
}
}
}
None
}
fn find_color_in_shape_representation(
repr_id: u32,
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
let repr = decoder.decode_by_id(repr_id).ok()?;
let items = get_refs_from_list(&repr, 3)?;
for item_id in items {
if let Some(style) = geometry_styles.get(&item_id) {
return Some(style.color);
}
}
None
}
fn find_geometry_item_color(
geometry_id: u32,
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
if let Some(style) = geometry_styles.get(&geometry_id) {
return Some(style.color);
}
let geom = decoder.decode_by_id(geometry_id).ok()?;
if geom.ifc_type != IfcType::IfcMappedItem {
return None;
}
let mapping_source_id = geom.get_ref(0)?;
let representation_map = decoder.decode_by_id(mapping_source_id).ok()?;
let mapped_representation_id = representation_map.get_ref(1)?;
let mapped_representation = decoder.decode_by_id(mapped_representation_id).ok()?;
let items = get_refs_from_list(&mapped_representation, 3)?;
for underlying in items {
if let Some(color) = find_geometry_item_color(underlying, geometry_styles, decoder) {
return Some(color);
}
}
None
}
fn extract_style_info_from_styled_item(
styled_item: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<GeometryStyleInfo> {
let style_refs = get_refs_from_list(styled_item, 1)?;
for style_id in style_refs {
if let Ok(style) = decoder.decode_by_id(style_id) {
if let Some(inner_refs) = get_refs_from_list(&style, 0) {
for inner_id in inner_refs {
if let Some(info) = extract_surface_style_info(inner_id, decoder) {
return Some(info);
}
}
}
if let Some(info) = extract_surface_style_info(style_id, decoder) {
return Some(info);
}
}
}
None
}
fn extract_surface_style_info(
style_id: u32,
decoder: &mut EntityDecoder,
) -> Option<GeometryStyleInfo> {
let style = decoder.decode_by_id(style_id).ok()?;
let material_name = normalize_style_name(style.get_string(0));
let (color, shading_color) = crate::style::extract_surface_style_colors(style_id, decoder)?;
Some(GeometryStyleInfo {
color,
shading_color,
material_name,
})
}
fn normalize_style_name(raw: Option<&str>) -> Option<String> {
let name = raw?.trim();
if name.is_empty() || name == "$" {
return None;
}
if name.eq_ignore_ascii_case("<unnamed>") || name.eq_ignore_ascii_case("unnamed") {
return None;
}
Some(name.to_string())
}
fn apply_opening_filter(
entity_jobs: &[EntityJob],
void_index: &FxHashMap<u32, Vec<u32>>,
filling_by_opening: &FxHashMap<u32, u32>,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
mode: OpeningFilterMode,
) -> (HashSet<u32>, FxHashMap<u32, Vec<u32>>) {
if mode == OpeningFilterMode::Default {
return (HashSet::default(), void_index.clone());
}
let filling_jobs: FxHashMap<u32, &EntityJob> = entity_jobs
.iter()
.filter(|job| matches!(job.ifc_type, IfcType::IfcWindow | IfcType::IfcDoor))
.map(|job| (job.id, job))
.collect();
if filling_jobs.is_empty() {
return (HashSet::default(), void_index.clone());
}
let mut skipped_entity_ids: HashSet<u32> = HashSet::default();
if mode == OpeningFilterMode::IgnoreAll {
for (&id, _) in &filling_jobs {
skipped_entity_ids.insert(id);
}
return (skipped_entity_ids, FxHashMap::default());
}
for (&id, job) in &filling_jobs {
if is_opaque_opening(job, geometry_style_index, decoder) {
skipped_entity_ids.insert(id);
}
}
if filling_by_opening.is_empty() {
return (skipped_entity_ids, void_index.clone());
}
let mut openings_to_suppress: HashSet<u32> = HashSet::default();
for (&opening_id, &filling_id) in filling_by_opening {
if skipped_entity_ids.contains(&filling_id) {
openings_to_suppress.insert(opening_id);
}
}
if openings_to_suppress.is_empty() {
return (skipped_entity_ids, void_index.clone());
}
let mut filtered: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
for (&host_id, openings) in void_index {
let remaining: Vec<u32> = openings
.iter()
.copied()
.filter(|oid| !openings_to_suppress.contains(oid))
.collect();
if !remaining.is_empty() {
filtered.insert(host_id, remaining);
}
}
(skipped_entity_ids, filtered)
}
fn is_opaque_opening(
job: &EntityJob,
styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> bool {
let Ok(entity) = decoder.decode_at(job.start, job.end) else {
return true;
};
if normalize_optional_string(entity.get_string(2))
.as_deref()
.map(|n| n.to_lowercase().contains("glas"))
.unwrap_or(false)
{
return false;
}
if job.element_color[3] < 1.0 {
return false;
}
let Some(product_shape_id) = entity.get_ref(6) else {
return true; };
let Ok(product_shape) = decoder.decode_by_id(product_shape_id) else {
return true;
};
let Some(repr_ids) = get_refs_from_list(&product_shape, 2) else {
return true;
};
for repr_id in repr_ids {
let Ok(repr) = decoder.decode_by_id(repr_id) else {
continue;
};
let Some(item_ids) = get_refs_from_list(&repr, 3) else {
continue;
};
for item_id in item_ids {
if let Some(style) = styles.get(&item_id) {
if has_glass_style(style) {
return false;
}
}
if let Ok(item) = decoder.decode_by_id(item_id) {
if item.ifc_type == IfcType::IfcMappedItem {
if let Some(source_id) = item.get_ref(0) {
if let Ok(source) = decoder.decode_by_id(source_id) {
if let Some(mapped_repr_id) = source.get_ref(1) {
if let Ok(mapped_repr) = decoder.decode_by_id(mapped_repr_id) {
if let Some(mapped_items) = get_refs_from_list(&mapped_repr, 3)
{
for mapped_item_id in mapped_items {
if let Some(style) = styles.get(&mapped_item_id) {
if has_glass_style(style) {
return false;
}
}
}
}
}
}
}
}
}
}
}
}
true }
fn has_glass_style(style: &GeometryStyleInfo) -> bool {
if style.color[3] < 1.0 {
return true;
}
if style
.material_name
.as_deref()
.map(|n| n.to_lowercase().contains("glas"))
.unwrap_or(false)
{
return true;
}
false
}
fn is_opening_with_subparts(ifc_type: &IfcType) -> bool {
matches!(ifc_type, IfcType::IfcWindow | IfcType::IfcDoor)
}
fn infer_opening_subpart_material_name(
ifc_type: &IfcType,
color: [f32; 4],
geometry_id: u32,
) -> Option<String> {
if !is_opening_with_subparts(ifc_type) {
return None;
}
let prefix = match ifc_type {
IfcType::IfcDoor => "Door",
_ => "Window",
};
if color[3] <= 0.65 {
return Some(format!("{}_Glass", prefix));
}
Some(format!("{}_Frame_{}", prefix, geometry_id))
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
}
#[test]
fn find_geometry_item_color_follows_mapped_item() {
const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
#100=IFCMAPPEDITEM(#101,#105);
#101=IFCREPRESENTATIONMAP(#102,#103);
#102=IFCAXIS2PLACEMENT3D(#104,$,$);
#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#110));
#104=IFCCARTESIANPOINT((0.,0.,0.));
#105=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#104,$,$);
ENDSEC;
END-ISO-10303-21;
"#;
let blue = [0.1, 0.2, 0.9, 1.0];
let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
styles.insert(
110,
GeometryStyleInfo {
color: blue,
shading_color: None,
material_name: None,
},
);
let mut decoder = EntityDecoder::new(IFC);
assert_eq!(
find_geometry_item_color(100, &styles, &mut decoder),
Some(blue)
);
assert_eq!(
find_geometry_item_color(110, &styles, &mut decoder),
Some(blue)
);
assert_eq!(find_geometry_item_color(101, &styles, &mut decoder), None);
}
}