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::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);
apply_inverse_rotation_point_f64(&mut mesh.origin, site_transform);
}
fn apply_inverse_rotation_point_f64(p: &mut [f64; 3], column_major_matrix: &[f64]) {
if column_major_matrix.len() < 16 || (p[0] == 0.0 && p[1] == 0.0 && p[2] == 0.0) {
return;
}
let (r00, r10, r20) = (
column_major_matrix[0],
column_major_matrix[1],
column_major_matrix[2],
);
let (r01, r11, r21) = (
column_major_matrix[4],
column_major_matrix[5],
column_major_matrix[6],
);
let (r02, r12, r22) = (
column_major_matrix[8],
column_major_matrix[9],
column_major_matrix[10],
);
let (x, y, z) = (p[0], p[1], p[2]);
p[0] = r00 * x + r10 * y + r20 * z;
p[1] = r01 * x + r11 * y + r21 * z;
p[2] = r02 * x + r12 * y + r22 * z;
}
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();
}
}
use crate::style::GeometryStyleInfo;
#[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>,
}
pub(crate) 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("IFCSPATIALZONE")
|| 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 prepass_spans = crate::prepass::PrepassSpans::default();
let mut project_id: Option<u32> = None;
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 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_referenced_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;
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") {
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(),
));
}
} else if 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_referenced_links.push((
parent_id,
args.get(4)
.map(|token| parse_step_ref_list(token))
.unwrap_or_default(),
));
}
}
}
if type_name == "IFCINDEXEDCOLOURMAP" {
prepass_spans.indexed_colour_maps.push((id, start, end));
continue;
}
if type_name == "IFCSTYLEDITEM" {
prepass_spans.styled_items.push((id, start, end));
continue;
} else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
prepass_spans.material_def_reprs.push((id, start, end));
continue;
} else if type_name == "IFCRELASSOCIATESMATERIAL" {
prepass_spans.rel_associates_material.push((id, start, end));
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" {
prepass_spans.void_rels.push((id, start, end));
} else if type_name == "IFCRELFILLSELEMENT" {
prepass_spans.fills_rels.push((id, start, end));
} else if type_name == "IFCRELAGGREGATES" {
prepass_spans.aggregate_rels.push((id, start, end));
} else if type_name == "IFCPROJECT" && project_id.is_none() {
project_id = Some(id);
} 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 {
for (rep_map_id, _class) in crate::element::plan_type_geometry(
rep_map_ids,
&referenced_representation_maps,
instantiated_type_ids.contains(type_id),
crate::element::TypeGeometryMode::SuppressInstanced,
) {
entity_jobs.push(EntityJob {
id: *type_id,
ifc_type: *ifc_type,
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),
});
}
}
let resolved = crate::prepass::resolve_prepass(
&prepass_spans,
&mut decoder,
crate::prepass::ResolveOptions {
collect_indexed_colour_full: true,
defer_attached_styles: defer_style_updates,
},
);
let crate::prepass::ResolvedPrepass {
mut geometry_style_index,
indexed_colour_index,
indexed_colour_full,
element_material_colors,
void_index,
filling_by_opening,
deferred_attached_styled_spans: deferred_styled_item_positions,
..
} = resolved;
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 !spatial_nodes.contains_key(&parent_id) {
continue;
}
for child_id in element_ids {
if spatial_nodes.contains_key(&child_id) {
let already_placed = spatial_nodes
.get(&child_id)
.is_some_and(|child| child.parent.is_some());
if !already_placed {
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);
}
}
} else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
parent.elements.push(child_id);
}
}
}
for (parent_id, element_ids) in quick_referenced_links {
if !spatial_nodes.contains_key(&parent_id) {
continue;
}
for child_id in element_ids {
if spatial_nodes.contains_key(&child_id) {
continue;
}
if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
parent.elements.push(child_id);
}
}
}
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 unit_scales = crate::prepass::resolve_unit_scales(content, project_id, &mut decoder);
decoder.seed_unit_scales(
unit_scales.length_unit_scale,
unit_scales.plane_angle_to_radians,
);
let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
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 seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
let void_index_arc = Arc::new(filtered_void_index);
let skipped_entity_ids = Arc::new(skipped_entity_ids);
crate::prepass::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_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,
seed_plane_angle_to_radians,
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 = {
let mut style_decoder =
EntityDecoder::with_arc_index(content, entity_index_arc.clone());
crate::prepass::resolve_styled_item_spans(
&deferred_styled_item_positions,
&mut style_decoder,
)
};
crate::prepass::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),
seed_plane_angle_to_radians: 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());
local_decoder.seed_unit_scales(unit_scale, seed_plane_angle_to_radians);
let entity = match local_decoder.decode_at(job.start, job.end) {
Ok(entity) => entity,
Err(_) => 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 metadata = crate::element::ElementMeshMetadata {
global_id: job.global_id.clone(),
name: job.name.clone(),
presentation_layer: job.presentation_layer.clone(),
space_zone_properties: job.space_zone_properties.clone(),
};
let kind = match job.representation_map_id {
Some(rep_map_id) => crate::element::ElementJobKind::TypeProduct {
rep_maps: vec![(rep_map_id, 1)],
},
None => crate::element::ElementJobKind::Product,
};
let ctx = crate::element::MeshProductionContext {
void_index,
geometry_style_index,
indexed_colour_full,
element_material_colors,
texture_index,
site_local_rotation,
};
let produced = crate::element::produce_element_meshes(
&crate::element::ElementMeshJob {
id: job.id,
ifc_type: job.ifc_type,
entity: &entity,
kind,
element_color: Some(job.element_color),
metadata: Some(&metadata),
},
&ctx,
&crate::element::MeshProductionOptions::default(),
&mut local_decoder,
&local_router,
);
if !produced.csg_failures.is_empty() {
if let Ok(mut collector) = csg_failure_collector.lock() {
for (product_id, fails) in produced.csg_failures {
collector.entry(product_id).or_default().extend(fails);
}
}
}
produced.meshes
}
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) = crate::element::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 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
}
#[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()
}
}