1use crate::types::mesh::MeshData;
10use crate::types::response::{
11 CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
12 QuickMetadataEntitySummary, QuickMetadataSpatialNode,
13};
14use ifc_lite_core::{
15 build_entity_index, AttributeValue, DecodedEntity, EntityDecoder,
16 EntityIndex, EntityScanner, IfcType,
17};
18use ifc_lite_geometry::TessellationQuality;
19use ifc_lite_geometry::{calculate_normals, GeometryRouter};
20use rayon::prelude::*;
21use rustc_hash::{FxHashMap, FxHashSet};
22use std::collections::{BTreeMap, HashMap, HashSet};
23use std::sync::Arc;
24
25#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum OpeningFilterMode {
29 #[default]
31 Default = 0,
32 IgnoreAll = 1,
34 IgnoreOpaque = 2,
36}
37
38impl OpeningFilterMode {
39 pub fn cache_key_suffix(&self) -> &'static str {
42 match self {
43 Self::Default => "default",
44 Self::IgnoreAll => "ignore_all",
45 Self::IgnoreOpaque => "ignore_opaque",
46 }
47 }
48}
49
50pub struct ProcessingResult {
52 pub meshes: Vec<MeshData>,
53 pub mesh_coordinate_space: Option<String>,
55 pub site_transform: Option<Vec<f64>>,
57 pub building_transform: Option<Vec<f64>>,
59 pub metadata: ModelMetadata,
60 pub stats: ProcessingStats,
61}
62
63#[derive(Debug, Clone, Copy)]
65pub struct StreamingOptions {
66 pub initial_batch_size: usize,
68 pub throughput_batch_size: usize,
70 pub fast_first_batch: bool,
72 pub include_properties: bool,
74 pub include_presentation_layers: bool,
76 pub emit_quick_metadata_bootstrap: bool,
78 pub retain_emitted_meshes: bool,
80 pub tessellation_quality: TessellationQuality,
87}
88
89impl Default for StreamingOptions {
90 fn default() -> Self {
91 Self {
92 initial_batch_size: 50,
93 throughput_batch_size: 50,
94 fast_first_batch: false,
95 include_properties: true,
96 include_presentation_layers: true,
97 emit_quick_metadata_bootstrap: false,
98 retain_emitted_meshes: true,
99 tessellation_quality: TessellationQuality::default(),
100 }
101 }
102}
103
104const SITE_LOCAL_MESH_COORDINATE_SPACE: &str = "site_local";
105const MODEL_RTC_MESH_COORDINATE_SPACE: &str = "model_rtc";
106const RAW_IFC_MESH_COORDINATE_SPACE: &str = "raw_ifc";
107
108const PLACEMENT_IDENTITY_EPSILON: f64 = 1e-9;
112
113#[inline]
114fn translation_is_nonidentity(t: (f64, f64, f64)) -> bool {
115 t.0.abs() > PLACEMENT_IDENTITY_EPSILON
116 || t.1.abs() > PLACEMENT_IDENTITY_EPSILON
117 || t.2.abs() > PLACEMENT_IDENTITY_EPSILON
118}
119
120fn apply_inverse_rotation_in_place(values: &mut [f32], column_major_matrix: &[f64]) {
125 if values.len() < 3 || column_major_matrix.len() < 16 {
126 return;
127 }
128
129 let r00 = column_major_matrix[0];
130 let r10 = column_major_matrix[1];
131 let r20 = column_major_matrix[2];
132 let r01 = column_major_matrix[4];
133 let r11 = column_major_matrix[5];
134 let r21 = column_major_matrix[6];
135 let r02 = column_major_matrix[8];
136 let r12 = column_major_matrix[9];
137 let r22 = column_major_matrix[10];
138
139 let is_identity = (r00 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
140 && r10.abs() < PLACEMENT_IDENTITY_EPSILON
141 && r20.abs() < PLACEMENT_IDENTITY_EPSILON
142 && r01.abs() < PLACEMENT_IDENTITY_EPSILON
143 && (r11 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
144 && r21.abs() < PLACEMENT_IDENTITY_EPSILON
145 && r02.abs() < PLACEMENT_IDENTITY_EPSILON
146 && r12.abs() < PLACEMENT_IDENTITY_EPSILON
147 && (r22 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON;
148 if is_identity {
149 return;
150 }
151
152 for chunk in values.chunks_exact_mut(3) {
153 let x = chunk[0] as f64;
154 let y = chunk[1] as f64;
155 let z = chunk[2] as f64;
156 chunk[0] = (r00 * x + r10 * y + r20 * z) as f32;
157 chunk[1] = (r01 * x + r11 * y + r21 * z) as f32;
158 chunk[2] = (r02 * x + r12 * y + r22 * z) as f32;
159 }
160}
161
162pub fn convert_mesh_to_site_local(mesh: &mut MeshData, site_transform: Option<&Vec<f64>>) {
169 let Some(site_transform) = site_transform else {
170 return;
171 };
172
173 apply_inverse_rotation_in_place(&mut mesh.positions, site_transform);
174 apply_inverse_rotation_in_place(&mut mesh.normals, site_transform);
175}
176
177struct EntityJob {
179 id: u32,
180 ifc_type: IfcType,
181 start: usize,
182 end: usize,
183 product_definition_shape_id: Option<u32>,
184 element_color: [f32; 4],
185 global_id: Option<String>,
186 name: Option<String>,
187 presentation_layer: Option<String>,
188 space_zone_properties: Option<BTreeMap<String, String>>,
189 representation_map_id: Option<u32>,
193}
194
195fn populate_entity_job_metadata(
196 job: &mut EntityJob,
197 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
198 element_material_color: &FxHashMap<u32, [f32; 4]>,
199 layer_by_assigned_representation: &FxHashMap<u32, String>,
200 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
201 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
202 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
203 decoder: &mut EntityDecoder,
204 include_presentation_layers: bool,
205) {
206 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
207 return;
208 }
209
210 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
211 return;
212 };
213
214 job.global_id = normalize_optional_string(entity.get_string(0));
215 job.name = normalize_optional_string(entity.get_string(2));
216 job.product_definition_shape_id = entity.get_ref(6);
217
218 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
219 return;
220 };
221
222 let resolved_color = color_cache_by_product_definition_shape
223 .entry(product_definition_shape_id)
224 .or_insert_with(|| {
225 resolve_element_color_for_product_definition_shape(
226 product_definition_shape_id,
227 geometry_style_index,
228 decoder,
229 )
230 });
231 if let Some(color) = resolved_color {
232 job.element_color = *color;
233 } else if let Some(color) = element_material_color.get(&job.id) {
234 job.element_color = *color;
235 }
236
237 if include_presentation_layers {
238 let resolved_layer = layer_cache_by_product_definition_shape
239 .entry(product_definition_shape_id)
240 .or_insert_with(|| {
241 resolve_presentation_layer_for_product_definition_shape(
242 product_definition_shape_id,
243 layer_by_assigned_representation,
244 layer_cache_by_representation,
245 decoder,
246 )
247 });
248 job.presentation_layer = resolved_layer.clone();
249 }
250}
251
252#[derive(Debug, Clone)]
253struct GeometryStyleInfo {
254 color: [f32; 4],
258 #[allow(dead_code)]
265 shading_color: Option<[f32; 4]>,
266 material_name: Option<String>,
267}
268
269#[derive(Debug, Clone)]
270struct PropertySetDefinition {
271 name: Option<String>,
272 property_ids: Vec<u32>,
273}
274
275#[derive(Debug, Clone)]
276struct RelDefinesByPropertiesLink {
277 property_set_id: u32,
278 related_object_ids: Vec<u32>,
279}
280
281fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
283 let list = entity.get_list(index)?;
284 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
285 if refs.is_empty() {
286 None
287 } else {
288 Some(refs)
289 }
290}
291
292fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
293 let value = raw?.trim();
294 if value.is_empty() || value == "$" {
295 return None;
296 }
297 Some(value.to_string())
298}
299
300fn normalize_ifc_property_name(raw: Option<&str>) -> Option<String> {
301 let name = normalize_optional_string(raw)?;
302 let cleaned = name.trim();
303 if cleaned.is_empty() {
304 return None;
305 }
306
307 Some(cleaned.to_string())
308}
309
310fn is_space_or_zone_type(ifc_type: &IfcType) -> bool {
311 matches!(
312 ifc_type,
313 IfcType::IfcSpace
314 | IfcType::IfcSpaceType
315 | IfcType::IfcZone
316 | IfcType::IfcSpatialZone
317 | IfcType::IfcSpatialZoneType
318 )
319}
320
321fn collect_property_set_definition(property_set: &DecodedEntity) -> Option<PropertySetDefinition> {
322 let property_ids = property_set
323 .get_list(4)
324 .or_else(|| property_set.get_list(2))
325 .map(|items| {
326 items
327 .iter()
328 .filter_map(AttributeValue::as_entity_ref)
329 .collect::<Vec<u32>>()
330 })
331 .unwrap_or_default();
332
333 if property_ids.is_empty() {
334 return None;
335 }
336
337 let name = normalize_optional_string(property_set.get_string(2))
338 .or_else(|| normalize_optional_string(property_set.get_string(0)));
339
340 Some(PropertySetDefinition { name, property_ids })
341}
342
343fn collect_rel_defines_by_properties_link(
344 rel_defines: &DecodedEntity,
345) -> Option<RelDefinesByPropertiesLink> {
346 let property_set_id = rel_defines.get_ref(5).or_else(|| rel_defines.get_ref(3))?;
347 let related_object_ids = rel_defines
348 .get_list(4)
349 .or_else(|| rel_defines.get_list(2))
350 .map(|items| {
351 items
352 .iter()
353 .filter_map(AttributeValue::as_entity_ref)
354 .collect::<Vec<u32>>()
355 })
356 .unwrap_or_default();
357
358 if related_object_ids.is_empty() {
359 return None;
360 }
361
362 Some(RelDefinesByPropertiesLink {
363 property_set_id,
364 related_object_ids,
365 })
366}
367
368fn attribute_list_to_string(values: &[AttributeValue]) -> Option<String> {
369 let tokens = values
370 .iter()
371 .filter_map(attribute_value_to_string)
372 .collect::<Vec<String>>();
373
374 if tokens.is_empty() {
375 return None;
376 }
377
378 Some(tokens.join("; "))
379}
380
381fn attribute_value_to_string(value: &AttributeValue) -> Option<String> {
382 match value {
383 AttributeValue::Null | AttributeValue::Derived => None,
384 AttributeValue::String(text) => normalize_optional_string(Some(text)),
385 AttributeValue::Enum(text) => normalize_optional_string(Some(text.trim_matches('.'))),
386 AttributeValue::Integer(number) => Some(number.to_string()),
387 AttributeValue::Float(number) => Some(number.to_string()),
388 AttributeValue::EntityRef(id) => Some(format!("#{id}")),
389 AttributeValue::List(values) => {
390 if values.len() >= 2 && matches!(values.first(), Some(AttributeValue::String(_))) {
391 return values.get(1).and_then(attribute_value_to_string);
392 }
393
394 attribute_list_to_string(values)
395 }
396 }
397}
398
399fn extract_property_name_and_value(property_entity: &DecodedEntity) -> Option<(String, String)> {
400 let property_name = normalize_ifc_property_name(property_entity.get_string(0))
401 .or_else(|| normalize_ifc_property_name(property_entity.get_string(2)))?;
402
403 let property_type = property_entity.ifc_type.name();
404 let value = match property_type {
405 "IfcPropertySingleValue" => property_entity.get(2).and_then(attribute_value_to_string),
406 "IfcPropertyEnumeratedValue" => property_entity.get(2).and_then(attribute_value_to_string),
407 "IfcPropertyListValue" => property_entity.get(2).and_then(attribute_value_to_string),
408 "IfcPropertyBoundedValue" => {
409 let lower = property_entity.get(2).and_then(attribute_value_to_string);
410 let upper = property_entity.get(3).and_then(attribute_value_to_string);
411 match (lower, upper) {
412 (Some(lo), Some(hi)) => Some(format!("{lo}..{hi}")),
413 (Some(lo), None) => Some(lo),
414 (None, Some(hi)) => Some(hi),
415 (None, None) => None,
416 }
417 }
418 "IfcPropertyReferenceValue" => property_entity.get(2).and_then(attribute_value_to_string),
419 _ => None,
420 }?;
421
422 let normalized_value = value.trim();
423 if normalized_value.is_empty() || normalized_value == "$" {
424 return None;
425 }
426
427 Some((property_name, normalized_value.to_string()))
428}
429
430fn add_space_zone_property(
431 attributes: &mut BTreeMap<String, String>,
432 property_set_name: Option<&str>,
433 property_name: &str,
434 property_value: &str,
435) {
436 if property_name.trim().is_empty() || property_value.trim().is_empty() {
437 return;
438 }
439
440 attributes
441 .entry(property_name.to_string())
442 .or_insert_with(|| property_value.to_string());
443
444 if let Some(pset_name) = normalize_optional_string(property_set_name) {
445 let scoped_name = format!("{}.{}", pset_name, property_name);
446 attributes
447 .entry(scoped_name)
448 .or_insert_with(|| property_value.to_string());
449 }
450}
451
452fn build_space_zone_properties_by_entity(
453 entity_jobs: &[EntityJob],
454 property_values_by_id: &FxHashMap<u32, (String, String)>,
455 property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
456 rel_defines_by_properties: &[RelDefinesByPropertiesLink],
457) -> FxHashMap<u32, BTreeMap<String, String>> {
458 let mut target_space_zone_ids = FxHashMap::default();
459 for job in entity_jobs
460 .iter()
461 .filter(|job| is_space_or_zone_type(&job.ifc_type))
462 {
463 target_space_zone_ids.insert(job.id, ());
464 }
465
466 if target_space_zone_ids.is_empty() {
467 return FxHashMap::default();
468 }
469
470 let mut properties_by_entity: FxHashMap<u32, BTreeMap<String, String>> = FxHashMap::default();
471
472 for link in rel_defines_by_properties {
473 let Some(property_set) = property_sets_by_id.get(&link.property_set_id) else {
474 continue;
475 };
476
477 for related_id in &link.related_object_ids {
478 if !target_space_zone_ids.contains_key(related_id) {
479 continue;
480 }
481
482 let attributes = properties_by_entity.entry(*related_id).or_default();
483 for property_id in &property_set.property_ids {
484 let Some((property_name, property_value)) = property_values_by_id.get(property_id)
485 else {
486 continue;
487 };
488
489 add_space_zone_property(
490 attributes,
491 property_set.name.as_deref(),
492 property_name,
493 property_value,
494 );
495 }
496 }
497 }
498
499 properties_by_entity
500}
501
502fn assign_space_zone_properties(
503 entity_jobs: &mut [EntityJob],
504 property_values_by_id: &FxHashMap<u32, (String, String)>,
505 property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
506 rel_defines_by_properties: &[RelDefinesByPropertiesLink],
507) {
508 let properties_by_entity = build_space_zone_properties_by_entity(
509 entity_jobs,
510 property_values_by_id,
511 property_sets_by_id,
512 rel_defines_by_properties,
513 );
514
515 if properties_by_entity.is_empty() {
516 return;
517 }
518
519 for job in entity_jobs.iter_mut() {
520 if let Some(properties) = properties_by_entity.get(&job.id) {
521 job.space_zone_properties = Some(properties.clone());
522 }
523 }
524}
525
526#[derive(Clone)]
527struct QuickSpatialNodeEntry {
528 express_id: u32,
529 type_name: String,
530 name: String,
531 elevation: Option<f64>,
532 children: Vec<u32>,
533 elements: Vec<u32>,
534 parent: Option<u32>,
535}
536
537#[inline]
539fn is_quick_spatial_type_ci(type_name: &str) -> bool {
540 type_name.eq_ignore_ascii_case("IFCPROJECT")
541 || type_name.eq_ignore_ascii_case("IFCSITE")
542 || type_name.eq_ignore_ascii_case("IFCBUILDING")
543 || type_name.eq_ignore_ascii_case("IFCBUILDINGSTOREY")
544 || type_name.eq_ignore_ascii_case("IFCSPACE")
545 || type_name.eq_ignore_ascii_case("IFCFACILITY")
546 || type_name.eq_ignore_ascii_case("IFCFACILITYPART")
547 || type_name.eq_ignore_ascii_case("IFCBRIDGE")
548 || type_name.eq_ignore_ascii_case("IFCBRIDGEPART")
549 || type_name.eq_ignore_ascii_case("IFCROAD")
550 || type_name.eq_ignore_ascii_case("IFCROADPART")
551 || type_name.eq_ignore_ascii_case("IFCRAILWAY")
552 || type_name.eq_ignore_ascii_case("IFCRAILWAYPART")
553}
554
555fn parse_step_arguments(entity_bytes: &[u8]) -> Vec<&[u8]> {
556 let Some(open_idx) = entity_bytes.iter().position(|byte| *byte == b'(') else {
557 return Vec::new();
558 };
559 let Some(close_idx) = entity_bytes.iter().rposition(|byte| *byte == b')') else {
560 return Vec::new();
561 };
562 if close_idx <= open_idx {
563 return Vec::new();
564 }
565 let args = &entity_bytes[open_idx + 1..close_idx];
566 let mut parts = Vec::new();
567 let mut in_string = false;
568 let mut depth = 0i32;
569 let mut start = 0usize;
570 let bytes = args;
571 let mut index = 0usize;
572 while index < bytes.len() {
573 match bytes[index] {
574 b'\'' => {
575 if in_string && index + 1 < bytes.len() && bytes[index + 1] == b'\'' {
576 index += 1;
577 } else {
578 in_string = !in_string;
579 }
580 }
581 b'(' if !in_string => depth += 1,
582 b')' if !in_string => depth -= 1,
583 b',' if !in_string && depth == 0 => {
584 parts.push(args[start..index].trim_ascii());
585 start = index + 1;
586 }
587 _ => {}
588 }
589 index += 1;
590 }
591 if start <= args.len() {
592 parts.push(args[start..].trim_ascii());
593 }
594 parts
595}
596
597fn parse_step_string(token: &[u8]) -> Option<String> {
598 let trimmed = token.trim_ascii();
599 if trimmed.len() < 2 || trimmed[0] != b'\'' || trimmed[trimmed.len() - 1] != b'\'' {
600 return None;
601 }
602 Some(String::from_utf8_lossy(&trimmed[1..trimmed.len() - 1]).replace("''", "'"))
603}
604
605fn parse_step_ref(token: &[u8]) -> Option<u32> {
606 std::str::from_utf8(token.trim_ascii().strip_prefix(b"#")?)
607 .ok()?
608 .parse()
609 .ok()
610}
611
612fn parse_step_ref_list(token: &[u8]) -> Vec<u32> {
613 let trimmed = token.trim_ascii();
614 let inner = trimmed
615 .strip_prefix(b"(")
616 .and_then(|value| value.strip_suffix(b")"))
617 .unwrap_or(trimmed);
618 inner.split(|byte| *byte == b',').filter_map(parse_step_ref).collect()
619}
620
621fn extract_name_from_args(args: &[&[u8]], fallback: &str) -> String {
622 args.get(2)
623 .and_then(|token| parse_step_string(token))
624 .filter(|value| !value.trim().is_empty())
625 .unwrap_or_else(|| fallback.to_string())
626}
627
628fn extract_storey_elevation_from_args(args: &[&[u8]]) -> Option<f64> {
629 for index in [9usize, 8usize] {
630 if let Some(value) = args
631 .get(index)
632 .and_then(|token| std::str::from_utf8(token.trim_ascii()).ok())
633 .and_then(|token| token.parse::<f64>().ok())
634 {
635 return Some(value);
636 }
637 }
638 args.iter()
639 .filter_map(|token| std::str::from_utf8(token.trim_ascii()).ok())
640 .filter_map(|token| token.parse::<f64>().ok())
641 .find(|value| value.abs() < 10_000.0)
642}
643
644fn build_quick_spatial_tree_node(
645 express_id: u32,
646 nodes: &HashMap<u32, QuickSpatialNodeEntry>,
647 element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
648) -> Result<QuickMetadataSpatialNode, String> {
649 let node = nodes
650 .get(&express_id)
651 .ok_or_else(|| format!("Quick spatial node #{express_id} not found"))?;
652 let mut children = Vec::with_capacity(node.children.len());
653 for child_id in &node.children {
654 children.push(build_quick_spatial_tree_node(
655 *child_id,
656 nodes,
657 element_summaries,
658 )?);
659 }
660 let elements = node
661 .elements
662 .iter()
663 .map(|element_id| {
664 element_summaries
665 .get(element_id)
666 .cloned()
667 .unwrap_or(QuickMetadataEntitySummary {
668 express_id: *element_id,
669 type_name: "IfcProduct".to_string(),
670 name: format!("IfcProduct #{}", element_id),
671 global_id: None,
672 kind: "element".to_string(),
673 has_children: false,
674 element_count: None,
675 elevation: None,
676 })
677 })
678 .collect();
679 Ok(QuickMetadataSpatialNode {
680 summary: QuickMetadataEntitySummary {
681 express_id: node.express_id,
682 type_name: node.type_name.clone(),
683 name: node.name.clone(),
684 global_id: None,
685 kind: "spatial".to_string(),
686 has_children: !node.children.is_empty() || !node.elements.is_empty(),
687 element_count: Some(node.elements.len()),
688 elevation: node.elevation,
689 },
690 children,
691 elements,
692 })
693}
694
695fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
696 match ifc_type {
697 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
698 IfcType::IfcSlab => 95,
699 IfcType::IfcColumn => 90,
700 IfcType::IfcBeam => 85,
701 IfcType::IfcRoof => 80,
702 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
703 IfcType::IfcCurtainWall => 70,
704 IfcType::IfcFooting | IfcType::IfcPile => 65,
705 IfcType::IfcDoor | IfcType::IfcWindow => 30,
706 IfcType::IfcFurnishingElement => 10,
707 _ => 50,
708 }
709}
710
711pub fn process_geometry<T>(content: &T) -> ProcessingResult
713where
714 T: AsRef<[u8]> + ?Sized,
715{
716 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
717}
718
719pub fn process_geometry_streaming(
721 content: &[u8],
722 batch_size: usize,
723 on_batch: impl FnMut(&[MeshData], usize, usize),
724) -> ProcessingResult {
725 process_geometry_streaming_with_options(
726 content,
727 StreamingOptions {
728 initial_batch_size: batch_size,
729 throughput_batch_size: batch_size,
730 ..StreamingOptions::default()
731 },
732 on_batch,
733 |_| {},
734 )
735}
736
737pub fn process_geometry_streaming_with_options(
739 content: &[u8],
740 options: StreamingOptions,
741 on_batch: impl FnMut(&[MeshData], usize, usize),
742 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
743) -> ProcessingResult {
744 process_geometry_streaming_with_options_and_bootstrap(
745 content,
746 options,
747 on_batch,
748 on_color_update,
749 |_| {},
750 )
751}
752
753pub fn process_geometry_streaming_with_options_and_bootstrap(
756 content: &[u8],
757 options: StreamingOptions,
758 on_batch: impl FnMut(&[MeshData], usize, usize),
759 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
760 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
761) -> ProcessingResult {
762 process_geometry_streaming_filtered_with_options(
763 content,
764 OpeningFilterMode::Default,
765 options,
766 on_batch,
767 on_color_update,
768 on_quick_metadata_bootstrap,
769 )
770}
771
772pub fn process_geometry_filtered<T>(
774 content: &T,
775 opening_filter: OpeningFilterMode,
776) -> ProcessingResult
777where
778 T: AsRef<[u8]> + ?Sized,
779{
780 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
781}
782
783pub fn process_geometry_filtered_with_quality<T>(
787 content: &T,
788 opening_filter: OpeningFilterMode,
789 tessellation_quality: TessellationQuality,
790) -> ProcessingResult
791where
792 T: AsRef<[u8]> + ?Sized,
793{
794 let content = content.as_ref();
795 process_geometry_streaming_filtered_with_options(
796 content,
797 opening_filter,
798 StreamingOptions {
799 initial_batch_size: usize::MAX,
800 throughput_batch_size: usize::MAX,
801 tessellation_quality,
802 ..StreamingOptions::default()
803 },
804 |_, _, _| {},
805 |_| {},
806 |_| {},
807 )
808}
809
810pub fn process_geometry_streaming_filtered(
812 content: &[u8],
813 opening_filter: OpeningFilterMode,
814 batch_size: usize,
815 on_batch: impl FnMut(&[MeshData], usize, usize),
816 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
817) -> ProcessingResult {
818 process_geometry_streaming_filtered_with_options(
819 content,
820 opening_filter,
821 StreamingOptions {
822 initial_batch_size: batch_size,
823 throughput_batch_size: batch_size,
824 ..StreamingOptions::default()
825 },
826 on_batch,
827 on_color_update,
828 |_| {},
829 )
830}
831
832pub fn process_geometry_streaming_filtered_with_options(
834 content: &[u8],
835 opening_filter: OpeningFilterMode,
836 options: StreamingOptions,
837 mut on_batch: impl FnMut(&[MeshData], usize, usize),
838 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
839 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
840) -> ProcessingResult {
841 let total_start = std::time::Instant::now();
842 let parse_start = std::time::Instant::now();
843 let entity_scan_start = std::time::Instant::now();
844
845 tracing::info!(
846 content_size = content.len(),
847 "Starting IFC geometry processing"
848 );
849
850 let entity_index = Arc::new(build_entity_index(content));
852 let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
853 tracing::debug!("Built entity index");
854
855 let mut geometry_style_index: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
856 let mut indexed_colour_index: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
861 let mut indexed_colour_full: FxHashMap<u32, crate::style::FullIndexedColourMap> =
862 FxHashMap::default();
863 let mut orphan_styled_items: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
867 let mut material_def_reprs: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
868 let mut element_to_material: FxHashMap<u32, u32> = FxHashMap::default();
869 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
870 let mut property_values_by_id: FxHashMap<u32, (String, String)> = FxHashMap::default();
871 let mut property_sets_by_id: FxHashMap<u32, PropertySetDefinition> = FxHashMap::default();
872 let mut rel_defines_by_properties: Vec<RelDefinesByPropertiesLink> = Vec::new();
873
874 let mut scanner = EntityScanner::new(content);
876 let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
877 let mut filling_by_opening: FxHashMap<u32, u32> = FxHashMap::default();
878 let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
883 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
884 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
889 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
890 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
899 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
900 let mut quick_spatial_nodes =
901 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
902 let mut quick_aggregate_links = if quick_metadata_enabled {
903 Vec::<(u32, Vec<u32>)>::new()
904 } else {
905 Vec::new()
906 };
907 let mut quick_containment_links = if quick_metadata_enabled {
908 Vec::<(u32, Vec<u32>)>::new()
909 } else {
910 Vec::new()
911 };
912 let mut quick_element_summaries = if quick_metadata_enabled {
913 HashMap::<u32, QuickMetadataEntitySummary>::new()
914 } else {
915 HashMap::new()
916 };
917 let mut schema_version = "IFC2X3".to_string();
918 let mut total_entities = 0usize;
919 let mut site_entity_pos: Option<(usize, usize)> = None;
920 let mut building_entity_pos: Option<(usize, usize)> = None;
921
922 let defer_style_updates = options.fast_first_batch
923 && opening_filter == OpeningFilterMode::Default
924 && !options.include_presentation_layers;
925 let mut deferred_styled_item_positions: Vec<(usize, usize)> = Vec::new();
926
927 while let Some((id, type_name, start, end)) = scanner.next_entity() {
928 total_entities += 1;
929 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
930 if is_quick_spatial_type_ci(type_name) {
932 let args = parse_step_arguments(&content[start..end]);
933 let fallback = format!("{type_name} #{id}");
934 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
935 express_id: id,
936 type_name: type_name.to_string(),
937 name: extract_name_from_args(&args, &fallback),
938 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
939 extract_storey_elevation_from_args(&args)
940 } else {
941 None
942 },
943 children: Vec::new(),
944 elements: Vec::new(),
945 parent: None,
946 });
947 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
948 let args = parse_step_arguments(&content[start..end]);
949 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
950 quick_aggregate_links.push((
951 parent_id,
952 args.get(5)
953 .map(|token| parse_step_ref_list(token))
954 .unwrap_or_default(),
955 ));
956 }
957 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE")
958 || type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE")
959 {
960 let args = parse_step_arguments(&content[start..end]);
961 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
962 quick_containment_links.push((
963 parent_id,
964 args.get(4)
965 .map(|token| parse_step_ref_list(token))
966 .unwrap_or_default(),
967 ));
968 }
969 }
970 }
971
972 if type_name == "IFCINDEXEDCOLOURMAP" {
973 if let Ok(icm) = decoder.decode_at(start, end) {
977 if let Some(full) =
978 crate::style::resolve_indexed_colour_map_full(&icm, &mut decoder)
979 {
980 let geometry_id = full.geometry_id;
981 indexed_colour_index
982 .entry(geometry_id)
983 .or_insert(full.dominant().to_array());
984 indexed_colour_full.entry(geometry_id).or_insert(full);
985 }
986 }
987 continue;
988 }
989
990 if type_name == "IFCSTYLEDITEM" {
991 if defer_style_updates {
992 if let Ok(styled_item) = decoder.decode_at(start, end) {
1001 if styled_item.get_ref(0).is_none() {
1002 if let Some(info) =
1003 extract_style_info_from_styled_item(&styled_item, &mut decoder)
1004 {
1005 orphan_styled_items.insert(id, info.color);
1006 }
1007 continue;
1008 }
1009 }
1010 deferred_styled_item_positions.push((start, end));
1012 continue;
1013 }
1014 if let Ok(styled_item) = decoder.decode_at(start, end) {
1015 if styled_item.get_ref(0).is_none() {
1016 if let Some(info) =
1019 extract_style_info_from_styled_item(&styled_item, &mut decoder)
1020 {
1021 orphan_styled_items.insert(id, info.color);
1022 }
1023 } else {
1024 collect_geometry_style_info(
1025 &mut geometry_style_index,
1026 &styled_item,
1027 &mut decoder,
1028 );
1029 }
1030 }
1031 continue;
1032 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
1033 if let Ok(entity) = decoder.decode_at(start, end) {
1035 if let Some(material_id) = entity.get_ref(3) {
1036 if let Some(reprs) = get_refs_from_list(&entity, 2) {
1037 material_def_reprs
1038 .entry(material_id)
1039 .or_default()
1040 .extend(reprs);
1041 }
1042 }
1043 }
1044 continue;
1045 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
1046 if let Ok(entity) = decoder.decode_at(start, end) {
1048 if let Some(material_select_id) = entity.get_ref(5) {
1049 if let Some(related) = get_refs_from_list(&entity, 4) {
1050 for element_id in related {
1051 element_to_material.insert(element_id, material_select_id);
1052 }
1053 }
1054 }
1055 }
1056 continue;
1057 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
1058 if !options.include_presentation_layers {
1059 continue;
1060 }
1061 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
1062 collect_presentation_layer_assignments(
1063 &mut presentation_layer_by_assigned_id,
1064 &layer_assignment,
1065 );
1066 }
1067 continue;
1068 } else if type_name == "IFCPROPERTYSET" {
1069 if !options.include_properties {
1070 continue;
1071 }
1072 if let Ok(property_set) = decoder.decode_at(start, end) {
1073 if let Some(definition) = collect_property_set_definition(&property_set) {
1074 property_sets_by_id.insert(id, definition);
1075 }
1076 }
1077 continue;
1078 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
1079 if !options.include_properties {
1080 continue;
1081 }
1082 if let Ok(rel_defines) = decoder.decode_at(start, end) {
1083 if let Some(link) = collect_rel_defines_by_properties_link(&rel_defines) {
1084 rel_defines_by_properties.push(link);
1085 }
1086 }
1087 continue;
1088 } else if type_name.starts_with("IFCPROPERTY") {
1089 if !options.include_properties {
1090 continue;
1091 }
1092 if let Ok(property_entity) = decoder.decode_at(start, end) {
1093 if let Some((name, value)) = extract_property_name_and_value(&property_entity) {
1094 property_values_by_id.insert(id, (name, value));
1095 }
1096 }
1097 continue;
1098 } else if type_name == "IFCRELVOIDSELEMENT" {
1099 if let Ok(entity) = decoder.decode_at(start, end) {
1100 if let (Some(host), Some(opening)) = (entity.get_ref(4), entity.get_ref(5)) {
1101 void_index.entry(host).or_default().push(opening);
1102 }
1103 }
1104 } else if type_name == "IFCRELFILLSELEMENT" {
1105 if let Ok(entity) = decoder.decode_at(start, end) {
1106 if let (Some(opening_id), Some(filling_id)) = (entity.get_ref(4), entity.get_ref(5))
1108 {
1109 filling_by_opening.insert(opening_id, filling_id);
1110 }
1111 }
1112 } else if type_name == "IFCRELAGGREGATES" {
1113 let args = parse_step_arguments(&content[start..end]);
1117 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
1118 let kids = args
1119 .get(5)
1120 .map(|token| parse_step_ref_list(token))
1121 .unwrap_or_default();
1122 if !kids.is_empty() {
1123 aggregate_children
1124 .entry(parent_id)
1125 .or_default()
1126 .extend(kids);
1127 }
1128 }
1129 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
1130 site_entity_pos = Some((start, end));
1131 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
1132 building_entity_pos = Some((start, end));
1133 }
1134
1135 if ifc_lite_core::has_geometry_by_name(type_name) {
1136 let ifc_type = IfcType::from_str(type_name);
1137 if quick_metadata_enabled {
1138 quick_element_summaries.insert(
1139 id,
1140 QuickMetadataEntitySummary {
1141 express_id: id,
1142 type_name: type_name.to_string(),
1143 name: format!("{type_name} #{id}"),
1144 global_id: None,
1145 kind: "element".to_string(),
1146 has_children: false,
1147 element_count: None,
1148 elevation: None,
1149 },
1150 );
1151 }
1152 entity_jobs.push(EntityJob {
1153 id,
1154 ifc_type: ifc_type.clone(),
1155 start,
1156 end,
1157 product_definition_shape_id: None,
1158 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
1159 global_id: None,
1160 name: None,
1161 presentation_layer: None,
1162 space_zone_properties: None,
1163 representation_map_id: None,
1164 });
1165 }
1166 else if type_name == "IFCMAPPEDITEM" {
1173 let args = parse_step_arguments(&content[start..end]);
1174 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
1175 referenced_representation_maps.insert(source_id);
1176 }
1177 } else if type_name == "IFCRELDEFINESBYTYPE" {
1178 let args = parse_step_arguments(&content[start..end]);
1181 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
1182 instantiated_type_ids.insert(type_id);
1183 }
1184 } else if (type_name.ends_with("TYPE") || type_name.ends_with("STYLE"))
1185 && IfcType::from_str(type_name).is_subtype_of(IfcType::IfcTypeProduct)
1186 {
1187 let args = parse_step_arguments(&content[start..end]);
1188 let rep_map_ids = args
1190 .get(6)
1191 .map(|token| parse_step_ref_list(token))
1192 .unwrap_or_default();
1193 if !rep_map_ids.is_empty() {
1194 type_product_geometry.push((
1195 id,
1196 start,
1197 end,
1198 IfcType::from_str(type_name),
1199 rep_map_ids,
1200 ));
1201 }
1202 }
1203 }
1204
1205 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
1213 if instantiated_type_ids.contains(type_id) {
1216 continue;
1217 }
1218 for &rep_map_id in rep_map_ids {
1219 if referenced_representation_maps.contains(&rep_map_id) {
1220 continue;
1221 }
1222 entity_jobs.push(EntityJob {
1223 id: *type_id,
1224 ifc_type: ifc_type.clone(),
1225 start: *start,
1226 end: *end,
1227 product_definition_shape_id: None,
1228 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
1229 global_id: None,
1230 name: None,
1231 presentation_layer: None,
1232 space_zone_properties: None,
1233 representation_map_id: Some(rep_map_id),
1234 });
1235 }
1236 }
1237
1238 ifc_lite_geometry::propagate_voids_via_aggregates(&mut void_index, &aggregate_children);
1245
1246 let entity_scan_time = entity_scan_start.elapsed();
1247
1248 let lookup_start = std::time::Instant::now();
1249 if options.include_properties {
1250 assign_space_zone_properties(
1251 &mut entity_jobs,
1252 &property_values_by_id,
1253 &property_sets_by_id,
1254 &rel_defines_by_properties,
1255 );
1256 }
1257 if options.fast_first_batch {
1258 entity_jobs.sort_by(|left, right| {
1259 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
1260 });
1261 }
1262 let lookup_time = lookup_start.elapsed();
1263
1264 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
1265 &entity_jobs,
1266 &void_index,
1267 &filling_by_opening,
1268 &geometry_style_index,
1269 &mut decoder,
1270 opening_filter,
1271 );
1272
1273 if content
1275 .windows(b"IFC4X3".len())
1276 .any(|window| window == b"IFC4X3")
1277 {
1278 schema_version = "IFC4X3".into();
1279 } else if content
1280 .windows(b"IFC4".len())
1281 .any(|window| window == b"IFC4")
1282 {
1283 schema_version = "IFC4".into();
1284 }
1285
1286 let geometry_entity_count = entity_jobs.len();
1287 tracing::info!(
1288 total_entities = total_entities,
1289 geometry_entities = geometry_entity_count,
1290 voids = void_index.len(),
1291 schema_version = %schema_version,
1292 "Entity scanning complete"
1293 );
1294
1295 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
1296 for (parent_id, child_ids) in quick_aggregate_links {
1297 if !spatial_nodes.contains_key(&parent_id) {
1298 continue;
1299 }
1300 for child_id in child_ids {
1301 if !spatial_nodes.contains_key(&child_id) {
1302 continue;
1303 }
1304 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1305 parent.children.push(child_id);
1306 }
1307 if let Some(child) = spatial_nodes.get_mut(&child_id) {
1308 child.parent = Some(parent_id);
1309 }
1310 }
1311 }
1312 for (parent_id, element_ids) in quick_containment_links {
1313 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1314 parent.elements.extend(element_ids);
1315 }
1316 }
1317 let mut root_id = spatial_nodes
1318 .values()
1319 .find(|node| node.type_name == "IfcProject")
1320 .map(|node| node.express_id);
1321 if root_id.is_none() {
1322 root_id = spatial_nodes
1323 .values()
1324 .find(|node| node.parent.is_none())
1325 .map(|node| node.express_id);
1326 }
1327 let spatial_tree = root_id
1328 .map(|root| {
1329 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1330 })
1331 .transpose()
1332 .unwrap_or(None);
1333 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1334 schema_version: schema_version.clone(),
1335 entity_count: total_entities,
1336 spatial_tree,
1337 });
1338 }
1339
1340 let preprocess_start = std::time::Instant::now();
1342 let mut router = GeometryRouter::with_units(content, &mut decoder);
1343 router.set_tessellation_quality(options.tessellation_quality);
1344
1345 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1347 let entity = decoder.decode_at(start, end).ok()?;
1348 let matrix = router
1349 .resolve_scaled_placement(&entity, &mut decoder)
1350 .ok()?;
1351 Some(matrix.to_vec())
1352 });
1353 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1354 let entity = decoder.decode_at(start, end).ok()?;
1355 let matrix = router
1356 .resolve_scaled_placement(&entity, &mut decoder)
1357 .ok()?;
1358 Some(matrix.to_vec())
1359 });
1360
1361 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1362 .iter()
1363 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1364 .collect();
1365 let detected_rtc_offset =
1366 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1367
1368 let site_rtc = site_transform
1377 .as_ref()
1378 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1380 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1381 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1382 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1383 } else if detected_has_offset {
1384 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1385 } else {
1386 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1387 };
1388 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1389 router.set_rtc_offset(rtc_offset);
1390 let preprocess_time = preprocess_start.elapsed();
1391
1392 let parse_time = parse_start.elapsed();
1393 tracing::info!(
1394 entity_scan_time_ms = entity_scan_time.as_millis(),
1395 lookup_time_ms = lookup_time.as_millis(),
1396 preprocess_time_ms = preprocess_time.as_millis(),
1397 parse_time_ms = parse_time.as_millis(),
1398 "Parse phase complete, starting geometry extraction"
1399 );
1400
1401 let geometry_start = std::time::Instant::now();
1403 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1405 let rtc_offset = router.rtc_offset();
1406 let void_index_arc = Arc::new(filtered_void_index);
1407 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1408 merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1411 let mut geometry_style_index = Arc::new(geometry_style_index);
1412 let indexed_colour_full = Arc::new(indexed_colour_full);
1413 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1418 content,
1419 &mut decoder,
1420 ));
1421 let element_material_colors = crate::style::build_element_material_colors(
1425 &material_def_reprs,
1426 &orphan_styled_items,
1427 &element_to_material,
1428 &mut decoder,
1429 );
1430 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1431 .iter()
1432 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1433 .collect();
1434 let element_material_colors = Arc::new(element_material_colors);
1435
1436 let total_jobs = entity_jobs.len();
1437 let initial_chunk_size = options.initial_batch_size.max(1);
1438 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1439 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1440 FxHashMap::default();
1441 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1442 FxHashMap::default();
1443 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1444 let mut meshes: Vec<MeshData> = Vec::new();
1445 let mut processed_jobs = 0usize;
1446 let mut total_meshes = 0usize;
1447 let mut total_vertices = 0usize;
1448 let mut total_triangles = 0usize;
1449 let mut chunk_start = 0usize;
1450 let mut current_chunk_size = initial_chunk_size;
1451
1452 let mut deferred_styles_applied = !defer_style_updates;
1453
1454 let csg_failure_collector: std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>> =
1457 std::sync::Mutex::new(FxHashMap::default());
1458
1459 while chunk_start < total_jobs {
1460 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1461 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1462
1463 #[cfg(not(target_arch = "wasm32"))]
1467 {
1468 let entity_index_for_meta = entity_index_arc.clone();
1470 jobs_chunk.par_iter_mut().for_each(|job| {
1471 if job.global_id.is_some()
1472 || job.name.is_some()
1473 || job.product_definition_shape_id.is_some()
1474 {
1475 return;
1476 }
1477 let mut local_decoder =
1478 EntityDecoder::with_arc_index(content, entity_index_for_meta.clone());
1479 let Ok(entity) = local_decoder.decode_at(job.start, job.end) else {
1480 return;
1481 };
1482 job.global_id = normalize_optional_string(entity.get_string(0));
1483 job.name = normalize_optional_string(entity.get_string(2));
1484 job.product_definition_shape_id = entity.get_ref(6);
1485 });
1486
1487 for job in jobs_chunk.iter_mut() {
1489 let Some(pds_id) = job.product_definition_shape_id else {
1490 continue;
1491 };
1492 let resolved_color = color_cache_by_product_definition_shape
1493 .entry(pds_id)
1494 .or_insert_with(|| {
1495 resolve_element_color_for_product_definition_shape(
1496 pds_id,
1497 &geometry_style_index,
1498 &mut decoder,
1499 )
1500 });
1501 if let Some(color) = resolved_color {
1502 job.element_color = *color;
1503 } else if let Some(color) = element_material_color.get(&job.id) {
1504 job.element_color = *color;
1507 }
1508 if options.include_presentation_layers {
1509 let resolved_layer = layer_cache_by_product_definition_shape
1510 .entry(pds_id)
1511 .or_insert_with(|| {
1512 resolve_presentation_layer_for_product_definition_shape(
1513 pds_id,
1514 &presentation_layer_by_assigned_id,
1515 &mut layer_cache_by_representation,
1516 &mut decoder,
1517 )
1518 });
1519 job.presentation_layer = resolved_layer.clone();
1520 }
1521 }
1522 }
1523
1524 #[cfg(target_arch = "wasm32")]
1526 for job in jobs_chunk.iter_mut() {
1527 populate_entity_job_metadata(
1528 job,
1529 &geometry_style_index,
1530 &element_material_color,
1531 &presentation_layer_by_assigned_id,
1532 &mut color_cache_by_product_definition_shape,
1533 &mut layer_cache_by_product_definition_shape,
1534 &mut layer_cache_by_representation,
1535 &mut decoder,
1536 options.include_presentation_layers,
1537 );
1538 }
1539 let site_local_rotation: Option<&Vec<f64>> =
1540 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1541 site_transform.as_ref()
1542 } else {
1543 None
1544 };
1545 let chunk_meshes: Vec<MeshData> = jobs_chunk
1546 .par_iter()
1547 .flat_map_iter(|job| {
1548 process_entity_job(
1549 job,
1550 content,
1551 &entity_index_arc,
1552 unit_scale,
1553 rtc_offset,
1554 options.tessellation_quality,
1555 void_index_arc.as_ref(),
1556 skipped_entity_ids.as_ref(),
1557 geometry_style_index.as_ref(),
1558 indexed_colour_full.as_ref(),
1559 element_material_colors.as_ref(),
1560 texture_index.as_ref(),
1561 site_local_rotation,
1562 &csg_failure_collector,
1563 )
1564 })
1565 .collect();
1566
1567 processed_jobs += jobs_chunk.len();
1568 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1569 total_triangles += chunk_meshes
1570 .iter()
1571 .map(|m| m.triangle_count())
1572 .sum::<usize>();
1573
1574 if !chunk_meshes.is_empty() {
1575 total_meshes += chunk_meshes.len();
1576 let emit_mesh_chunk_size = current_chunk_size.max(1);
1577 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1578 on_batch(emitted_meshes, processed_jobs, total_jobs);
1579 }
1580 if options.retain_emitted_meshes {
1581 meshes.extend(chunk_meshes);
1582 }
1583
1584 if !deferred_styles_applied {
1585 let mut rebuilt_styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
1588 {
1589 let mut style_decoder =
1590 EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1591 for &(start, end) in &deferred_styled_item_positions {
1592 if let Ok(styled_item) = style_decoder.decode_at(start, end) {
1593 collect_geometry_style_info(
1594 &mut rebuilt_styles,
1595 &styled_item,
1596 &mut style_decoder,
1597 );
1598 }
1599 }
1600 }
1601 merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1602 geometry_style_index = Arc::new(rebuilt_styles);
1603 let deferred_color_updates = build_color_updates_for_jobs(
1604 &entity_jobs[..processed_jobs],
1605 geometry_style_index.as_ref(),
1606 content,
1607 &entity_index_arc,
1608 );
1609 if !deferred_color_updates.is_empty() {
1610 on_color_update(&deferred_color_updates);
1611 }
1612 deferred_styles_applied = true;
1613 }
1614 }
1615 chunk_start = chunk_end;
1616 current_chunk_size = throughput_chunk_size;
1617 }
1618
1619 let geometry_time = geometry_start.elapsed();
1620 let csg_failures = csg_failure_collector
1623 .into_inner()
1624 .unwrap_or_else(|poisoned| poisoned.into_inner());
1625 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1626 let products_with_failures = csg_failures.len();
1627 if total_csg_failures > 0 {
1628 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1629 for fails in csg_failures.values() {
1630 for f in fails {
1631 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1632 }
1633 }
1634 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1635 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1636 let breakdown = breakdown
1637 .iter()
1638 .map(|(reason, count)| format!("{reason}={count}"))
1639 .collect::<Vec<_>>()
1640 .join(" ");
1641 tracing::warn!(
1642 total_csg_failures,
1643 products_with_failures,
1644 %breakdown,
1645 "CSG failures during geometry extraction (cut dropped, host kept uncut)"
1646 );
1647 }
1648
1649 let total_time = total_start.elapsed();
1650
1651 tracing::info!(
1652 meshes = meshes.len(),
1653 vertices = total_vertices,
1654 triangles = total_triangles,
1655 geometry_time_ms = geometry_time.as_millis(),
1656 total_time_ms = total_time.as_millis(),
1657 "Geometry processing complete"
1658 );
1659
1660 ProcessingResult {
1661 meshes,
1662 mesh_coordinate_space: Some(coord_space.to_string()),
1663 site_transform,
1664 building_transform,
1665 metadata: ModelMetadata {
1666 schema_version,
1667 entity_count: total_entities,
1668 geometry_entity_count,
1669 coordinate_info: CoordinateInfo {
1670 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1671 is_geo_referenced: has_rtc_offset,
1672 },
1673 length_unit_scale: Some(unit_scale),
1674 georeferencing: crate::extract_georeferencing(content),
1675 },
1676 stats: ProcessingStats {
1677 total_meshes,
1678 total_vertices,
1679 total_triangles,
1680 parse_time_ms: parse_time.as_millis() as u64,
1681 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1682 lookup_time_ms: lookup_time.as_millis() as u64,
1683 preprocess_time_ms: preprocess_time.as_millis() as u64,
1684 geometry_time_ms: geometry_time.as_millis() as u64,
1685 total_time_ms: total_time.as_millis() as u64,
1686 from_cache: false,
1687 total_csg_failures: total_csg_failures as u64,
1688 products_with_failures: products_with_failures as u64,
1689 },
1690 }
1691}
1692
1693fn process_entity_job(
1694 job: &EntityJob,
1695 content: &[u8],
1696 entity_index_arc: &Arc<EntityIndex>,
1697 unit_scale: f64,
1698 rtc_offset: (f64, f64, f64),
1699 tessellation_quality: TessellationQuality,
1700 void_index: &FxHashMap<u32, Vec<u32>>,
1701 skipped_entity_ids: &HashSet<u32>,
1702 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
1703 indexed_colour_full: &FxHashMap<u32, crate::style::FullIndexedColourMap>,
1704 element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
1705 texture_index: &FxHashMap<u32, ifc_lite_geometry::ResolvedTextureMap>,
1708 site_local_rotation: Option<&Vec<f64>>,
1711 csg_failure_collector: &std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>>,
1714) -> Vec<MeshData> {
1715 if skipped_entity_ids.contains(&job.id) {
1716 return Vec::new();
1717 }
1718
1719 let mut local_decoder = EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1720
1721 let entity = match local_decoder.decode_at(job.start, job.end) {
1722 Ok(entity) => entity,
1723 Err(_) => return Vec::new(),
1724 };
1725
1726 let has_representation = entity.get(6).is_some_and(|a| !a.is_null());
1727 if !has_representation {
1728 return Vec::new();
1729 }
1730
1731 let mut local_router = GeometryRouter::with_scale_and_quality(unit_scale, tessellation_quality);
1732 local_router.set_rtc_offset(rtc_offset);
1733 let local_router = local_router;
1734 let result = (|| -> Vec<MeshData> {
1735 let global_id = job.global_id.clone();
1736 let name = job.name.clone();
1737 let presentation_layer = job.presentation_layer.clone();
1738 let space_zone_properties = job.space_zone_properties.clone();
1739 let element_color = job.element_color;
1740
1741 if let Some(rep_map_id) = job.representation_map_id {
1745 return process_type_representation_map_job(
1746 job,
1747 rep_map_id,
1748 &local_router,
1749 &mut local_decoder,
1750 geometry_style_index,
1751 texture_index,
1752 element_color,
1753 global_id,
1754 name,
1755 presentation_layer,
1756 site_local_rotation,
1757 );
1758 }
1759
1760 let has_openings = void_index.get(&job.id).is_some_and(|v| !v.is_empty());
1761
1762 let mut emit_sub_meshes = |sub_meshes: ifc_lite_geometry::SubMeshCollection,
1767 local_decoder: &mut EntityDecoder|
1768 -> Vec<MeshData> {
1769 let mut out: Vec<MeshData> = Vec::with_capacity(sub_meshes.len());
1770 let material_colors = element_material_colors.get(&job.id);
1774 let mut mat_color_idx = 0usize;
1775
1776 for sub in sub_meshes.sub_meshes {
1777 let mut sub_mesh = sub.mesh;
1778 if sub_mesh.is_empty() {
1779 continue;
1780 }
1781
1782 if sub_mesh.normals.is_empty() {
1783 calculate_normals(&mut sub_mesh);
1784 }
1785
1786 let style = geometry_style_index.get(&sub.geometry_id);
1787 let direct_color = style.map(|s| s.color).or_else(|| {
1790 find_geometry_item_color(sub.geometry_id, geometry_style_index, local_decoder)
1791 });
1792 let color = crate::style::resolve_submesh_color(
1793 direct_color,
1794 material_colors.map(|v| v.as_slice()),
1795 &mut mat_color_idx,
1796 element_color,
1797 );
1798 let material_name = style
1799 .and_then(|s| s.material_name.as_ref())
1800 .map(ToString::to_string);
1801 let material_name = material_name.or_else(|| {
1802 infer_opening_subpart_material_name(&job.ifc_type, color, sub.geometry_id)
1803 });
1804
1805 let mut mesh_data = MeshData::new(
1806 job.id,
1807 job.ifc_type.name().to_string(),
1808 sub_mesh.positions,
1809 sub_mesh.normals,
1810 sub_mesh.indices,
1811 color,
1812 )
1813 .with_element_metadata(global_id.clone(), name.clone(), presentation_layer.clone())
1814 .with_properties(space_zone_properties.clone())
1815 .with_style_metadata(material_name, Some(sub.geometry_id));
1816 convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
1817 out.push(mesh_data);
1818 }
1819 out
1820 };
1821
1822 if has_openings {
1823 if let Ok(sub_meshes) = local_router.process_element_with_submeshes_and_voids(
1828 &entity,
1829 &mut local_decoder,
1830 void_index,
1831 ) {
1832 if !sub_meshes.is_empty() {
1833 let out = emit_sub_meshes(sub_meshes, &mut local_decoder);
1834 if !out.is_empty() {
1835 return out;
1836 }
1837 }
1838 }
1839 } else {
1840 let has_indexed_colour = !indexed_colour_full.is_empty()
1843 && find_indexed_colour_for_element(&entity, indexed_colour_full, &mut local_decoder)
1844 .is_some();
1845 if !has_indexed_colour {
1846 if let Ok(sub_meshes) =
1852 local_router.process_element_with_submeshes(&entity, &mut local_decoder)
1853 {
1854 if !sub_meshes.is_empty() {
1855 let out = emit_sub_meshes(sub_meshes, &mut local_decoder);
1856 if !out.is_empty() {
1857 return out;
1858 }
1859 }
1860 }
1861 }
1862 }
1863
1864 let _ = local_router.take_csg_failures();
1874
1875 let mut mesh_candidate = local_router
1876 .process_element_with_voids(&entity, &mut local_decoder, void_index)
1877 .ok();
1878 let needs_fallback = match mesh_candidate.as_ref() {
1879 Some(mesh) => mesh.is_empty(),
1880 None => true,
1881 };
1882 if needs_fallback {
1883 mesh_candidate = local_router
1884 .process_element(&entity, &mut local_decoder)
1885 .ok();
1886 }
1887
1888 if let Some(mut mesh) = mesh_candidate {
1889 if !mesh.is_empty() {
1890 if !indexed_colour_full.is_empty() {
1895 if let Some(full) = find_indexed_colour_for_element(
1896 &entity,
1897 indexed_colour_full,
1898 &mut local_decoder,
1899 ) {
1900 let geometry_id = full.geometry_id;
1901 if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&mesh, full) {
1902 let mut out: Vec<MeshData> = Vec::with_capacity(groups.len());
1903 for (color, mut part) in groups {
1904 if part.normals.is_empty() {
1905 calculate_normals(&mut part);
1906 }
1907 let mut mesh_data = MeshData::new(
1908 job.id,
1909 job.ifc_type.name().to_string(),
1910 part.positions,
1911 part.normals,
1912 part.indices,
1913 color.to_array(),
1914 )
1915 .with_element_metadata(
1916 global_id.clone(),
1917 name.clone(),
1918 presentation_layer.clone(),
1919 )
1920 .with_properties(space_zone_properties.clone())
1921 .with_style_metadata(None, Some(geometry_id));
1922 convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
1923 out.push(mesh_data);
1924 }
1925 if !out.is_empty() {
1926 return out;
1927 }
1928 }
1929 }
1930 }
1931
1932 if mesh.normals.is_empty() {
1933 calculate_normals(&mut mesh);
1934 }
1935
1936 let mut mesh_data = MeshData::new(
1937 job.id,
1938 job.ifc_type.name().to_string(),
1939 mesh.positions,
1940 mesh.normals,
1941 mesh.indices,
1942 element_color,
1943 )
1944 .with_element_metadata(global_id, name, presentation_layer)
1945 .with_properties(space_zone_properties);
1946 convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
1947 return vec![mesh_data];
1948 }
1949 }
1950
1951 Vec::new()
1952 })();
1953
1954 let failures = local_router.take_csg_failures();
1961 if !failures.is_empty() {
1962 if let Ok(mut collector) = csg_failure_collector.lock() {
1963 for (product_id, fails) in failures {
1964 collector.entry(product_id).or_default().extend(fails);
1965 }
1966 }
1967 }
1968
1969 result
1970}
1971
1972#[allow(clippy::too_many_arguments)]
1981fn process_type_representation_map_job(
1982 job: &EntityJob,
1983 rep_map_id: u32,
1984 router: &GeometryRouter,
1985 decoder: &mut EntityDecoder,
1986 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
1987 texture_index: &FxHashMap<u32, ifc_lite_geometry::ResolvedTextureMap>,
1988 element_color: [f32; 4],
1989 global_id: Option<String>,
1990 name: Option<String>,
1991 presentation_layer: Option<String>,
1992 site_local_rotation: Option<&Vec<f64>>,
1993) -> Vec<MeshData> {
1994 let Ok(rep_map) = decoder.decode_by_id(rep_map_id) else {
1995 return Vec::new();
1996 };
1997 let Ok(parts) =
2001 router.process_representation_map_with_texture(&rep_map, decoder, texture_index)
2002 else {
2003 return Vec::new();
2004 };
2005 if parts.is_empty() {
2006 return Vec::new();
2007 }
2008
2009 let color = resolve_color_for_representation_map(rep_map_id, geometry_style_index, decoder)
2010 .unwrap_or(element_color);
2011
2012 let mut out: Vec<MeshData> = Vec::with_capacity(parts.len());
2013 for (mut mesh, uvs, texture) in parts {
2014 if mesh.is_empty() {
2015 continue;
2016 }
2017 if mesh.normals.is_empty() {
2018 calculate_normals(&mut mesh);
2019 }
2020 let mut mesh_data = MeshData::new(
2021 job.id,
2022 job.ifc_type.name().to_string(),
2023 mesh.positions,
2024 mesh.normals,
2025 mesh.indices,
2026 color,
2027 )
2028 .with_element_metadata(global_id.clone(), name.clone(), presentation_layer.clone());
2029
2030 if let Some(tex) = texture {
2033 mesh_data = mesh_data.with_texture(
2034 uvs,
2035 crate::types::mesh::MeshTextureData {
2036 rgba: tex.rgba,
2037 width: tex.width,
2038 height: tex.height,
2039 repeat_s: tex.repeat_s,
2040 repeat_t: tex.repeat_t,
2041 },
2042 );
2043 }
2044
2045 convert_mesh_to_site_local(&mut mesh_data, site_local_rotation);
2046 out.push(mesh_data);
2047 }
2048 out
2049}
2050
2051fn resolve_color_for_representation_map(
2056 rep_map_id: u32,
2057 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
2058 decoder: &mut EntityDecoder,
2059) -> Option<[f32; 4]> {
2060 let rep_map = decoder.decode_by_id(rep_map_id).ok()?;
2061 let mapped_rep_id = rep_map.get_ref(1)?;
2063 let mapped_rep = decoder.decode_by_id(mapped_rep_id).ok()?;
2064 let item_ids = get_refs_from_list(&mapped_rep, 3)?;
2066 for item_id in item_ids {
2067 if let Some(style) = geometry_style_index.get(&item_id) {
2068 return Some(style.color);
2069 }
2070 if let Some(color) = find_geometry_item_color(item_id, geometry_style_index, decoder) {
2071 return Some(color);
2072 }
2073 }
2074 None
2075}
2076
2077fn find_indexed_colour_for_element<'a>(
2081 entity: &DecodedEntity,
2082 indexed_colour_full: &'a FxHashMap<u32, crate::style::FullIndexedColourMap>,
2083 decoder: &mut EntityDecoder,
2084) -> Option<&'a crate::style::FullIndexedColourMap> {
2085 let pds_id = entity.get_ref(6)?;
2086 let pds = decoder.decode_by_id(pds_id).ok()?;
2087 let repr_ids = get_refs_from_list(&pds, 2)?;
2088 for repr_id in repr_ids {
2089 if let Ok(repr) = decoder.decode_by_id(repr_id) {
2090 if let Some(items) = get_refs_from_list(&repr, 3) {
2091 for item_id in items {
2092 if let Some(full) = indexed_colour_full.get(&item_id) {
2093 return Some(full);
2094 }
2095 }
2096 }
2097 }
2098 }
2099 None
2100}
2101
2102fn merge_indexed_colours(
2106 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
2107 indexed_colours: &FxHashMap<u32, [f32; 4]>,
2108) {
2109 for (&geometry_id, &color) in indexed_colours {
2110 geometry_styles
2111 .entry(geometry_id)
2112 .or_insert_with(|| GeometryStyleInfo {
2113 color,
2114 shading_color: None,
2115 material_name: None,
2116 });
2117 }
2118}
2119
2120fn collect_geometry_style_info(
2121 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
2122 styled_item: &DecodedEntity,
2123 decoder: &mut EntityDecoder,
2124) {
2125 let Some(geometry_id) = styled_item.get_ref(0) else {
2126 return;
2127 };
2128
2129 if geometry_styles.contains_key(&geometry_id) {
2130 return;
2131 }
2132
2133 if let Some(style_info) = extract_style_info_from_styled_item(styled_item, decoder) {
2134 geometry_styles.insert(geometry_id, style_info);
2135 }
2136}
2137
2138fn build_color_updates_for_jobs(
2139 jobs: &[EntityJob],
2140 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2141 content: &[u8],
2142 entity_index: &Arc<EntityIndex>,
2143) -> Vec<(u32, [f32; 4])> {
2144 let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
2145 let mut updates: Vec<(u32, [f32; 4])> = Vec::new();
2146
2147 for job in jobs {
2148 if let Some(rep_map_id) = job.representation_map_id {
2154 if let Some(color) =
2155 resolve_color_for_representation_map(rep_map_id, geometry_styles, &mut decoder)
2156 {
2157 if color != job.element_color {
2158 updates.push((job.id, color));
2159 }
2160 }
2161 continue;
2162 }
2163 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
2164 continue;
2165 };
2166 let Some(product_definition_shape_id) = entity.get_ref(6) else {
2167 continue;
2168 };
2169 let Some(color) = resolve_element_color_for_product_definition_shape(
2170 product_definition_shape_id,
2171 geometry_styles,
2172 &mut decoder,
2173 ) else {
2174 continue;
2175 };
2176 if color != job.element_color {
2177 updates.push((job.id, color));
2178 }
2179 }
2180
2181 updates
2182}
2183
2184fn collect_presentation_layer_assignments(
2185 layer_by_assigned_representation: &mut FxHashMap<u32, String>,
2186 layer_assignment: &DecodedEntity,
2187) {
2188 let Some(layer_name) = normalize_optional_string(layer_assignment.get_string(0)) else {
2189 return;
2190 };
2191
2192 let Some(assigned_items) = get_refs_from_list(layer_assignment, 2) else {
2193 return;
2194 };
2195
2196 for assigned in assigned_items {
2197 layer_by_assigned_representation
2198 .entry(assigned)
2199 .or_insert_with(|| layer_name.clone());
2200 }
2201}
2202
2203fn resolve_element_color_for_product_definition_shape(
2204 product_definition_shape_id: u32,
2205 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2206 decoder: &mut EntityDecoder,
2207) -> Option<[f32; 4]> {
2208 find_color_in_representation(product_definition_shape_id, geometry_styles, decoder)
2209}
2210
2211fn resolve_presentation_layer_for_product_definition_shape(
2212 product_definition_shape_id: u32,
2213 layer_by_assigned_representation: &FxHashMap<u32, String>,
2214 cache_by_representation: &mut FxHashMap<u32, Option<String>>,
2215 decoder: &mut EntityDecoder,
2216) -> Option<String> {
2217 if let Some(layer_name) = layer_by_assigned_representation.get(&product_definition_shape_id) {
2218 return Some(layer_name.clone());
2219 }
2220
2221 let product_definition_shape = decoder.decode_by_id(product_definition_shape_id).ok()?;
2222 let representation_ids = get_refs_from_list(&product_definition_shape, 2)?;
2223
2224 for representation_id in representation_ids {
2225 if let Some(layer_name) = resolve_presentation_layer_name(
2226 representation_id,
2227 layer_by_assigned_representation,
2228 cache_by_representation,
2229 decoder,
2230 &mut Vec::new(),
2231 ) {
2232 return Some(layer_name);
2233 }
2234 }
2235
2236 None
2237}
2238
2239fn resolve_presentation_layer_name(
2240 representation_id: u32,
2241 layer_by_assigned_representation: &FxHashMap<u32, String>,
2242 cache_by_representation: &mut FxHashMap<u32, Option<String>>,
2243 decoder: &mut EntityDecoder,
2244 traversal_stack: &mut Vec<u32>,
2245) -> Option<String> {
2246 if let Some(cached) = cache_by_representation.get(&representation_id) {
2247 return cached.clone();
2248 }
2249
2250 if traversal_stack.contains(&representation_id) {
2251 return None;
2252 }
2253 traversal_stack.push(representation_id);
2254
2255 if let Some(layer_name) = layer_by_assigned_representation.get(&representation_id) {
2256 let result = Some(layer_name.clone());
2257 cache_by_representation.insert(representation_id, result.clone());
2258 traversal_stack.pop();
2259 return result;
2260 }
2261
2262 let mut resolved: Option<String> = None;
2263
2264 if let Ok(representation) = decoder.decode_by_id(representation_id) {
2265 if let Some(items) = get_refs_from_list(&representation, 3) {
2266 for item_id in items {
2267 if let Some(layer_name) = layer_by_assigned_representation.get(&item_id) {
2268 resolved = Some(layer_name.clone());
2269 break;
2270 }
2271
2272 if let Ok(item) = decoder.decode_by_id(item_id) {
2273 if item.ifc_type == IfcType::IfcMappedItem {
2274 if let Some(mapping_source_id) = item.get_ref(0) {
2275 if let Ok(mapping_source) = decoder.decode_by_id(mapping_source_id) {
2276 if let Some(mapped_representation_id) = mapping_source.get_ref(1) {
2277 if let Some(layer_name) = resolve_presentation_layer_name(
2278 mapped_representation_id,
2279 layer_by_assigned_representation,
2280 cache_by_representation,
2281 decoder,
2282 traversal_stack,
2283 ) {
2284 resolved = Some(layer_name);
2285 break;
2286 }
2287 }
2288 }
2289 }
2290 }
2291 }
2292 }
2293 }
2294 }
2295
2296 traversal_stack.pop();
2297 cache_by_representation.insert(representation_id, resolved.clone());
2298 resolved
2299}
2300
2301fn find_color_in_representation(
2303 repr_id: u32,
2304 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2305 decoder: &mut EntityDecoder,
2306) -> Option<[f32; 4]> {
2307 let repr = decoder.decode_by_id(repr_id).ok()?;
2309
2310 let repr_list = get_refs_from_list(&repr, 2)?;
2312
2313 for shape_repr_id in repr_list {
2314 if let Ok(shape_repr) = decoder.decode_by_id(shape_repr_id) {
2315 if let Some(items) = get_refs_from_list(&shape_repr, 3) {
2317 for item_id in items {
2318 if let Some(style) = geometry_styles.get(&item_id) {
2320 return Some(style.color);
2321 }
2322
2323 if let Ok(item) = decoder.decode_by_id(item_id) {
2325 if item.ifc_type == IfcType::IfcMappedItem {
2326 if let Some(source_id) = item.get_ref(0) {
2327 if let Ok(source) = decoder.decode_by_id(source_id) {
2328 if let Some(mapped_repr_id) = source.get_ref(1) {
2329 if let Some(color) = find_color_in_shape_representation(
2330 mapped_repr_id,
2331 geometry_styles,
2332 decoder,
2333 ) {
2334 return Some(color);
2335 }
2336 }
2337 }
2338 }
2339 }
2340 }
2341 }
2342 }
2343 }
2344 }
2345
2346 None
2347}
2348
2349fn find_color_in_shape_representation(
2351 repr_id: u32,
2352 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2353 decoder: &mut EntityDecoder,
2354) -> Option<[f32; 4]> {
2355 let repr = decoder.decode_by_id(repr_id).ok()?;
2356 let items = get_refs_from_list(&repr, 3)?;
2357
2358 for item_id in items {
2359 if let Some(style) = geometry_styles.get(&item_id) {
2360 return Some(style.color);
2361 }
2362 }
2363
2364 None
2365}
2366
2367fn find_geometry_item_color(
2374 geometry_id: u32,
2375 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2376 decoder: &mut EntityDecoder,
2377) -> Option<[f32; 4]> {
2378 if let Some(style) = geometry_styles.get(&geometry_id) {
2380 return Some(style.color);
2381 }
2382
2383 let geom = decoder.decode_by_id(geometry_id).ok()?;
2386 if geom.ifc_type != IfcType::IfcMappedItem {
2387 return None;
2388 }
2389 let mapping_source_id = geom.get_ref(0)?;
2391 let representation_map = decoder.decode_by_id(mapping_source_id).ok()?;
2393 let mapped_representation_id = representation_map.get_ref(1)?;
2394 let mapped_representation = decoder.decode_by_id(mapped_representation_id).ok()?;
2395 let items = get_refs_from_list(&mapped_representation, 3)?;
2397 for underlying in items {
2398 if let Some(color) = find_geometry_item_color(underlying, geometry_styles, decoder) {
2399 return Some(color);
2400 }
2401 }
2402 None
2403}
2404
2405fn extract_style_info_from_styled_item(
2407 styled_item: &DecodedEntity,
2408 decoder: &mut EntityDecoder,
2409) -> Option<GeometryStyleInfo> {
2410 let style_refs = get_refs_from_list(styled_item, 1)?;
2411
2412 for style_id in style_refs {
2413 if let Ok(style) = decoder.decode_by_id(style_id) {
2414 if let Some(inner_refs) = get_refs_from_list(&style, 0) {
2416 for inner_id in inner_refs {
2417 if let Some(info) = extract_surface_style_info(inner_id, decoder) {
2418 return Some(info);
2419 }
2420 }
2421 }
2422
2423 if let Some(info) = extract_surface_style_info(style_id, decoder) {
2425 return Some(info);
2426 }
2427 }
2428 }
2429
2430 None
2431}
2432
2433fn extract_surface_style_info(
2440 style_id: u32,
2441 decoder: &mut EntityDecoder,
2442) -> Option<GeometryStyleInfo> {
2443 let style = decoder.decode_by_id(style_id).ok()?;
2444 let material_name = normalize_style_name(style.get_string(0));
2445 let (color, shading_color) = crate::style::extract_surface_style_colors(style_id, decoder)?;
2446 Some(GeometryStyleInfo {
2447 color,
2448 shading_color,
2449 material_name,
2450 })
2451}
2452
2453fn normalize_style_name(raw: Option<&str>) -> Option<String> {
2454 let name = raw?.trim();
2455 if name.is_empty() || name == "$" {
2456 return None;
2457 }
2458
2459 if name.eq_ignore_ascii_case("<unnamed>") || name.eq_ignore_ascii_case("unnamed") {
2460 return None;
2461 }
2462
2463 Some(name.to_string())
2464}
2465
2466fn apply_opening_filter(
2472 entity_jobs: &[EntityJob],
2473 void_index: &FxHashMap<u32, Vec<u32>>,
2474 filling_by_opening: &FxHashMap<u32, u32>,
2475 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
2476 decoder: &mut EntityDecoder,
2477 mode: OpeningFilterMode,
2478) -> (HashSet<u32>, FxHashMap<u32, Vec<u32>>) {
2479 if mode == OpeningFilterMode::Default {
2480 return (HashSet::default(), void_index.clone());
2481 }
2482
2483 let filling_jobs: FxHashMap<u32, &EntityJob> = entity_jobs
2485 .iter()
2486 .filter(|job| matches!(job.ifc_type, IfcType::IfcWindow | IfcType::IfcDoor))
2487 .map(|job| (job.id, job))
2488 .collect();
2489
2490 if filling_jobs.is_empty() {
2491 return (HashSet::default(), void_index.clone());
2492 }
2493
2494 let mut skipped_entity_ids: HashSet<u32> = HashSet::default();
2495
2496 if mode == OpeningFilterMode::IgnoreAll {
2501 for (&id, _) in &filling_jobs {
2502 skipped_entity_ids.insert(id);
2503 }
2504 return (skipped_entity_ids, FxHashMap::default());
2505 }
2506
2507 for (&id, job) in &filling_jobs {
2511 if is_opaque_opening(job, geometry_style_index, decoder) {
2512 skipped_entity_ids.insert(id);
2513 }
2514 }
2515
2516 if filling_by_opening.is_empty() {
2517 return (skipped_entity_ids, void_index.clone());
2519 }
2520
2521 let mut openings_to_suppress: HashSet<u32> = HashSet::default();
2523 for (&opening_id, &filling_id) in filling_by_opening {
2524 if skipped_entity_ids.contains(&filling_id) {
2525 openings_to_suppress.insert(opening_id);
2526 }
2527 }
2528
2529 if openings_to_suppress.is_empty() {
2530 return (skipped_entity_ids, void_index.clone());
2531 }
2532
2533 let mut filtered: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
2534 for (&host_id, openings) in void_index {
2535 let remaining: Vec<u32> = openings
2536 .iter()
2537 .copied()
2538 .filter(|oid| !openings_to_suppress.contains(oid))
2539 .collect();
2540 if !remaining.is_empty() {
2541 filtered.insert(host_id, remaining);
2542 }
2543 }
2544
2545 (skipped_entity_ids, filtered)
2546}
2547
2548fn is_opaque_opening(
2556 job: &EntityJob,
2557 styles: &FxHashMap<u32, GeometryStyleInfo>,
2558 decoder: &mut EntityDecoder,
2559) -> bool {
2560 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
2561 return true;
2562 };
2563
2564 if normalize_optional_string(entity.get_string(2))
2566 .as_deref()
2567 .map(|n| n.to_lowercase().contains("glas"))
2568 .unwrap_or(false)
2569 {
2570 return false;
2571 }
2572
2573 if job.element_color[3] < 1.0 {
2577 return false;
2578 }
2579
2580 let Some(product_shape_id) = entity.get_ref(6) else {
2581 return true; };
2583
2584 let Ok(product_shape) = decoder.decode_by_id(product_shape_id) else {
2585 return true;
2586 };
2587
2588 let Some(repr_ids) = get_refs_from_list(&product_shape, 2) else {
2589 return true;
2590 };
2591
2592 for repr_id in repr_ids {
2593 let Ok(repr) = decoder.decode_by_id(repr_id) else {
2594 continue;
2595 };
2596 let Some(item_ids) = get_refs_from_list(&repr, 3) else {
2597 continue;
2598 };
2599 for item_id in item_ids {
2600 if let Some(style) = styles.get(&item_id) {
2602 if has_glass_style(style) {
2603 return false;
2604 }
2605 }
2606
2607 if let Ok(item) = decoder.decode_by_id(item_id) {
2609 if item.ifc_type == IfcType::IfcMappedItem {
2610 if let Some(source_id) = item.get_ref(0) {
2611 if let Ok(source) = decoder.decode_by_id(source_id) {
2612 if let Some(mapped_repr_id) = source.get_ref(1) {
2613 if let Ok(mapped_repr) = decoder.decode_by_id(mapped_repr_id) {
2614 if let Some(mapped_items) = get_refs_from_list(&mapped_repr, 3)
2615 {
2616 for mapped_item_id in mapped_items {
2617 if let Some(style) = styles.get(&mapped_item_id) {
2618 if has_glass_style(style) {
2619 return false;
2620 }
2621 }
2622 }
2623 }
2624 }
2625 }
2626 }
2627 }
2628 }
2629 }
2630 }
2631 }
2632
2633 true }
2635
2636fn has_glass_style(style: &GeometryStyleInfo) -> bool {
2642 if style.color[3] < 1.0 {
2643 return true;
2644 }
2645 if style
2646 .material_name
2647 .as_deref()
2648 .map(|n| n.to_lowercase().contains("glas"))
2649 .unwrap_or(false)
2650 {
2651 return true;
2652 }
2653 false
2654}
2655
2656fn is_opening_with_subparts(ifc_type: &IfcType) -> bool {
2657 matches!(ifc_type, IfcType::IfcWindow | IfcType::IfcDoor)
2658}
2659
2660fn infer_opening_subpart_material_name(
2661 ifc_type: &IfcType,
2662 color: [f32; 4],
2663 geometry_id: u32,
2664) -> Option<String> {
2665 if !is_opening_with_subparts(ifc_type) {
2666 return None;
2667 }
2668
2669 let prefix = match ifc_type {
2670 IfcType::IfcDoor => "Door",
2671 _ => "Window",
2672 };
2673
2674 if color[3] <= 0.65 {
2676 return Some(format!("{}_Glass", prefix));
2677 }
2678
2679 Some(format!("{}_Frame_{}", prefix, geometry_id))
2680}
2681
2682#[cfg(test)]
2687mod tests {
2688 use super::*;
2689
2690 fn map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
2691 pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
2692 }
2693
2694 #[test]
2695 fn find_geometry_item_color_follows_mapped_item() {
2696 const IFC: &str = r#"ISO-10303-21;
2701HEADER;
2702FILE_DESCRIPTION((''),'2;1');
2703FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
2704FILE_SCHEMA(('IFC4'));
2705ENDSEC;
2706DATA;
2707#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
2708#100=IFCMAPPEDITEM(#101,#105);
2709#101=IFCREPRESENTATIONMAP(#102,#103);
2710#102=IFCAXIS2PLACEMENT3D(#104,$,$);
2711#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#110));
2712#104=IFCCARTESIANPOINT((0.,0.,0.));
2713#105=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#104,$,$);
2714ENDSEC;
2715END-ISO-10303-21;
2716"#;
2717 let blue = [0.1, 0.2, 0.9, 1.0];
2718 let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
2719 styles.insert(
2720 110,
2721 GeometryStyleInfo {
2722 color: blue,
2723 shading_color: None,
2724 material_name: None,
2725 },
2726 );
2727
2728 let mut decoder = EntityDecoder::new(IFC);
2729
2730 assert_eq!(
2732 find_geometry_item_color(100, &styles, &mut decoder),
2733 Some(blue)
2734 );
2735 assert_eq!(
2737 find_geometry_item_color(110, &styles, &mut decoder),
2738 Some(blue)
2739 );
2740 assert_eq!(find_geometry_item_color(101, &styles, &mut decoder), None);
2742 }
2743
2744}