1use crate::types::mesh::{InstanceRecord, MeshData, RawInstanceOccurrence};
10use crate::types::response::{
11 CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
12 QuickMetadataEntitySummary,
13};
14use ifc_lite_core::{
15 DecodedEntity, EntityDecoder,
16 EntityIndex, EntityScanner, IfcType,
17};
18use ifc_lite_geometry::TessellationQuality;
19use ifc_lite_geometry::GeometryRouter;
20use rayon::prelude::*;
21use rustc_hash::{FxHashMap, FxHashSet};
22use std::collections::{BTreeMap, HashMap, HashSet};
23use std::sync::Arc;
24
25mod color_layer;
26mod diagnostics;
27mod entity_index;
28use entity_index::{IndexBuilder, ProcessingIndex};
29pub(crate) mod instancing;
30mod jobs;
31mod opening_filter;
32mod properties;
33mod quick_metadata;
34mod site_local;
35
36pub use quick_metadata::is_quick_spatial_type_ci;
37pub use site_local::convert_mesh_to_site_local;
38
39use jobs::{build_color_updates_for_jobs, process_entity_job};
40
41use color_layer::{
42 collect_presentation_layer_assignments, resolve_element_color_for_product_definition_shape,
43 resolve_presentation_layer_for_product_definition_shape,
44};
45use opening_filter::apply_opening_filter;
46use properties::resolve_space_zone_properties_lazy;
47use quick_metadata::{
48 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
49 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
50};
51use site_local::{
52 translation_is_nonidentity, MODEL_RTC_MESH_COORDINATE_SPACE, RAW_IFC_MESH_COORDINATE_SPACE,
53 SITE_LOCAL_MESH_COORDINATE_SPACE,
54};
55
56#[cfg(not(target_arch = "wasm32"))]
63use std::time::Instant as Clock;
64
65#[cfg(target_arch = "wasm32")]
66#[derive(Clone, Copy)]
67struct Clock;
68
69#[cfg(target_arch = "wasm32")]
70impl Clock {
71 #[inline]
72 fn now() -> Self {
73 Clock
74 }
75 #[inline]
76 fn elapsed(&self) -> std::time::Duration {
77 std::time::Duration::ZERO
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum OpeningFilterMode {
85 #[default]
87 Default = 0,
88 IgnoreAll = 1,
90 IgnoreOpaque = 2,
92}
93
94impl OpeningFilterMode {
95 pub fn cache_key_suffix(&self) -> &'static str {
98 match self {
99 Self::Default => "default",
100 Self::IgnoreAll => "ignore_all",
101 Self::IgnoreOpaque => "ignore_opaque",
102 }
103 }
104}
105
106pub struct ProcessingResult {
108 pub meshes: Vec<MeshData>,
109 pub instances: Vec<InstanceRecord>,
116 pub mesh_coordinate_space: Option<String>,
118 pub site_transform: Option<Vec<f64>>,
120 pub building_transform: Option<Vec<f64>>,
122 pub metadata: ModelMetadata,
123 pub stats: ProcessingStats,
124}
125
126#[derive(Debug, Clone)]
130pub struct StreamingOptions {
131 pub initial_batch_size: usize,
133 pub throughput_batch_size: usize,
135 pub fast_first_batch: bool,
137 pub include_properties: bool,
139 pub include_presentation_layers: bool,
141 pub emit_quick_metadata_bootstrap: bool,
143 pub retain_emitted_meshes: bool,
145 pub tessellation_quality: TessellationQuality,
152 pub entity_index: Option<Arc<EntityIndex>>,
160 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
166 pub enable_instancing: bool,
175}
176
177impl Default for StreamingOptions {
178 fn default() -> Self {
179 Self {
180 initial_batch_size: 50,
181 throughput_batch_size: 50,
182 fast_first_batch: false,
183 include_properties: true,
184 include_presentation_layers: true,
185 emit_quick_metadata_bootstrap: false,
186 retain_emitted_meshes: true,
187 tessellation_quality: TessellationQuality::default(),
188 entity_index: None,
189 cancel: None,
190 enable_instancing: false,
191 }
192 }
193}
194
195pub(super) struct EntityJob {
197 pub(super) id: u32,
198 pub(super) ifc_type: IfcType,
199 pub(super) start: usize,
200 pub(super) end: usize,
201 pub(super) product_definition_shape_id: Option<u32>,
202 pub(super) element_color: [f32; 4],
203 pub(super) global_id: Option<String>,
204 pub(super) name: Option<String>,
205 pub(super) presentation_layer: Option<String>,
206 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
207 pub(super) representation_map_id: Option<u32>,
211}
212
213#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
215#[allow(clippy::too_many_arguments)]
217fn populate_entity_job_metadata(
218 job: &mut EntityJob,
219 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
220 element_material_color: &FxHashMap<u32, [f32; 4]>,
221 layer_by_assigned_representation: &FxHashMap<u32, String>,
222 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
223 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
224 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
225 decoder: &mut EntityDecoder,
226 include_presentation_layers: bool,
227) {
228 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
229 return;
230 }
231
232 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
233 return;
234 };
235
236 job.global_id = normalize_optional_string(entity.get_string(0));
237 job.name = normalize_optional_string(entity.get_string(2));
238 job.product_definition_shape_id = entity.get_ref(6);
239
240 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
241 return;
242 };
243
244 let resolved_color = color_cache_by_product_definition_shape
245 .entry(product_definition_shape_id)
246 .or_insert_with(|| {
247 resolve_element_color_for_product_definition_shape(
248 product_definition_shape_id,
249 geometry_style_index,
250 decoder,
251 )
252 });
253 if let Some(color) = resolved_color {
254 job.element_color = *color;
255 } else if let Some(color) = element_material_color.get(&job.id) {
256 job.element_color = *color;
257 }
258
259 if include_presentation_layers {
260 let resolved_layer = layer_cache_by_product_definition_shape
261 .entry(product_definition_shape_id)
262 .or_insert_with(|| {
263 resolve_presentation_layer_for_product_definition_shape(
264 product_definition_shape_id,
265 layer_by_assigned_representation,
266 layer_cache_by_representation,
267 decoder,
268 )
269 });
270 job.presentation_layer = resolved_layer.clone();
271 }
272}
273
274use crate::style::GeometryStyleInfo;
278
279pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
281 let list = entity.get_list(index)?;
282 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
283 if refs.is_empty() {
284 None
285 } else {
286 Some(refs)
287 }
288}
289
290pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
291 let value = raw?.trim();
292 if value.is_empty() || value == "$" {
293 return None;
294 }
295 Some(value.to_string())
296}
297
298fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
299 match ifc_type {
300 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
301 IfcType::IfcSlab => 95,
302 IfcType::IfcColumn => 90,
303 IfcType::IfcBeam => 85,
304 IfcType::IfcRoof => 80,
305 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
306 IfcType::IfcCurtainWall => 70,
307 IfcType::IfcFooting | IfcType::IfcPile => 65,
308 IfcType::IfcDoor | IfcType::IfcWindow => 30,
309 IfcType::IfcFurnishingElement => 10,
310 _ => 50,
311 }
312}
313
314pub fn process_geometry<T>(content: &T) -> ProcessingResult
316where
317 T: AsRef<[u8]> + ?Sized,
318{
319 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
320}
321
322pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
329where
330 T: AsRef<[u8]> + ?Sized,
331{
332 process_geometry_streaming_filtered_with_options(
333 content.as_ref(),
334 OpeningFilterMode::Default,
335 StreamingOptions {
336 initial_batch_size: usize::MAX,
337 throughput_batch_size: usize::MAX,
338 entity_index: Some(index),
339 ..StreamingOptions::default()
340 },
341 |_, _, _| {},
342 |_| {},
343 |_| {},
344 )
345}
346
347pub fn process_geometry_streaming(
349 content: &[u8],
350 batch_size: usize,
351 on_batch: impl FnMut(&[MeshData], usize, usize),
352) -> ProcessingResult {
353 process_geometry_streaming_with_options(
354 content,
355 StreamingOptions {
356 initial_batch_size: batch_size,
357 throughput_batch_size: batch_size,
358 ..StreamingOptions::default()
359 },
360 on_batch,
361 |_| {},
362 )
363}
364
365pub fn process_geometry_streaming_with_options(
367 content: &[u8],
368 options: StreamingOptions,
369 on_batch: impl FnMut(&[MeshData], usize, usize),
370 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
371) -> ProcessingResult {
372 process_geometry_streaming_with_options_and_bootstrap(
373 content,
374 options,
375 on_batch,
376 on_color_update,
377 |_| {},
378 )
379}
380
381pub fn process_geometry_streaming_with_options_and_bootstrap(
384 content: &[u8],
385 options: StreamingOptions,
386 on_batch: impl FnMut(&[MeshData], usize, usize),
387 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
388 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
389) -> ProcessingResult {
390 process_geometry_streaming_filtered_with_options(
391 content,
392 OpeningFilterMode::Default,
393 options,
394 on_batch,
395 on_color_update,
396 on_quick_metadata_bootstrap,
397 )
398}
399
400pub fn process_geometry_filtered<T>(
402 content: &T,
403 opening_filter: OpeningFilterMode,
404) -> ProcessingResult
405where
406 T: AsRef<[u8]> + ?Sized,
407{
408 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
409}
410
411pub fn process_geometry_filtered_with_quality<T>(
415 content: &T,
416 opening_filter: OpeningFilterMode,
417 tessellation_quality: TessellationQuality,
418) -> ProcessingResult
419where
420 T: AsRef<[u8]> + ?Sized,
421{
422 let content = content.as_ref();
423 process_geometry_streaming_filtered_with_options(
424 content,
425 opening_filter,
426 StreamingOptions {
427 initial_batch_size: usize::MAX,
428 throughput_batch_size: usize::MAX,
429 tessellation_quality,
430 ..StreamingOptions::default()
431 },
432 |_, _, _| {},
433 |_| {},
434 |_| {},
435 )
436}
437
438pub fn process_geometry_streaming_filtered(
440 content: &[u8],
441 opening_filter: OpeningFilterMode,
442 batch_size: usize,
443 on_batch: impl FnMut(&[MeshData], usize, usize),
444 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
445) -> ProcessingResult {
446 process_geometry_streaming_filtered_with_options(
447 content,
448 opening_filter,
449 StreamingOptions {
450 initial_batch_size: batch_size,
451 throughput_batch_size: batch_size,
452 ..StreamingOptions::default()
453 },
454 on_batch,
455 on_color_update,
456 |_| {},
457 )
458}
459
460pub fn process_geometry_streaming_filtered_with_options(
462 content: &[u8],
463 opening_filter: OpeningFilterMode,
464 options: StreamingOptions,
465 mut on_batch: impl FnMut(&[MeshData], usize, usize),
466 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
467 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
468) -> ProcessingResult {
469 let total_start = Clock::now();
470 let parse_start = Clock::now();
471 let entity_scan_start = Clock::now();
472
473 let pipeline_span = tracing::info_span!(
480 "geometry_pipeline",
481 byte_size = content.len(),
482 element_count = tracing::field::Empty,
483 total_ms = tracing::field::Empty,
484 );
485 let _pipeline_guard = pipeline_span.clone().entered();
486
487 tracing::info!(
488 content_size = content.len(),
489 "Starting IFC geometry processing"
490 );
491
492 let scan_span = tracing::info_span!(
493 "scan_prepass",
494 total_entities = tracing::field::Empty,
495 geometry_entities = tracing::field::Empty,
496 phase_ms = tracing::field::Empty,
497 );
498 let scan_guard = scan_span.clone().entered();
499
500 let provided_index = options.entity_index.clone();
507 let building_index = provided_index.is_none();
508 let mut inline_index = IndexBuilder::new(content.len(), building_index);
509 let mut decoder = match &provided_index {
510 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
511 None => EntityDecoder::new(content),
512 };
513 tracing::debug!("Entity index will be built inline during the scan");
514
515 let mut prepass_spans = crate::prepass::PrepassSpans::default();
521 let mut project_id: Option<u32> = None;
522 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
523 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
530
531 let mut scanner = EntityScanner::new(content);
533 let mut georeferencing_candidates = Vec::new();
534 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
535 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
540 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
541 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
546 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
555 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
556 let mut quick_spatial_nodes =
557 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
558 let mut quick_aggregate_links = if quick_metadata_enabled {
559 Vec::<(u32, Vec<u32>)>::new()
560 } else {
561 Vec::new()
562 };
563 let mut quick_containment_links = if quick_metadata_enabled {
564 Vec::<(u32, Vec<u32>)>::new()
565 } else {
566 Vec::new()
567 };
568 let mut quick_referenced_links = if quick_metadata_enabled {
573 Vec::<(u32, Vec<u32>)>::new()
574 } else {
575 Vec::new()
576 };
577 let mut quick_element_summaries = if quick_metadata_enabled {
578 HashMap::<u32, QuickMetadataEntitySummary>::new()
579 } else {
580 HashMap::new()
581 };
582 let mut schema_version = "IFC2X3".to_string();
583 let mut total_entities = 0usize;
584 let mut site_entity_pos: Option<(usize, usize)> = None;
585 let mut building_entity_pos: Option<(usize, usize)> = None;
586
587 let defer_style_updates = options.fast_first_batch
588 && opening_filter == OpeningFilterMode::Default
589 && !options.include_presentation_layers;
590
591 while let Some((id, type_name, start, end)) = scanner.next_entity() {
592 total_entities += 1;
593 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
594 georeferencing_candidates.push((id, ifc_type));
595 }
596 if building_index {
597 inline_index.insert(id, (start, end));
598 }
599 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
600 if is_quick_spatial_type_ci(type_name) {
602 let args = parse_step_arguments(&content[start..end]);
603 let fallback = format!("{type_name} #{id}");
604 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
605 express_id: id,
606 type_name: type_name.to_string(),
607 name: extract_name_from_args(&args, &fallback),
608 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
609 extract_storey_elevation_from_args(&args)
610 } else {
611 None
612 },
613 children: Vec::new(),
614 elements: Vec::new(),
615 parent: None,
616 });
617 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
618 let args = parse_step_arguments(&content[start..end]);
619 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
620 quick_aggregate_links.push((
621 parent_id,
622 args.get(5)
623 .map(|token| parse_step_ref_list(token))
624 .unwrap_or_default(),
625 ));
626 }
627 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
628 let args = parse_step_arguments(&content[start..end]);
629 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
630 quick_containment_links.push((
631 parent_id,
632 args.get(4)
633 .map(|token| parse_step_ref_list(token))
634 .unwrap_or_default(),
635 ));
636 }
637 } else if type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
638 let args = parse_step_arguments(&content[start..end]);
639 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
640 quick_referenced_links.push((
641 parent_id,
642 args.get(4)
643 .map(|token| parse_step_ref_list(token))
644 .unwrap_or_default(),
645 ));
646 }
647 }
648 }
649
650 if type_name == "IFCINDEXEDCOLOURMAP" {
651 prepass_spans.indexed_colour_maps.push((id, start, end));
653 continue;
654 }
655
656 if type_name == "IFCSTYLEDITEM" {
657 prepass_spans.styled_items.push((id, start, end));
661 continue;
662 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
663 prepass_spans.material_def_reprs.push((id, start, end));
664 continue;
665 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
666 prepass_spans.rel_associates_material.push((id, start, end));
667 continue;
668 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
669 if !options.include_presentation_layers {
670 continue;
671 }
672 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
673 collect_presentation_layer_assignments(
674 &mut presentation_layer_by_assigned_id,
675 &layer_assignment,
676 );
677 }
678 continue;
679 } else if type_name == "IFCPROPERTYSET" {
680 continue;
683 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
684 if options.include_properties {
685 rel_defines_spans.push((start, end));
686 }
687 continue;
688 } else if type_name.starts_with("IFCPROPERTY") {
689 continue;
692 } else if type_name == "IFCRELVOIDSELEMENT" {
693 prepass_spans.void_rels.push((id, start, end));
694 } else if type_name == "IFCRELFILLSELEMENT" {
695 prepass_spans.fills_rels.push((id, start, end));
696 } else if type_name == "IFCRELAGGREGATES" {
697 prepass_spans.aggregate_rels.push((id, start, end));
702 } else if type_name == "IFCPROJECT" && project_id.is_none() {
703 project_id = Some(id);
704 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
705 site_entity_pos = Some((start, end));
706 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
707 building_entity_pos = Some((start, end));
708 }
709
710 if ifc_lite_core::has_geometry_by_name(type_name) {
711 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
715 if quick_metadata_enabled {
716 quick_element_summaries.insert(
717 id,
718 QuickMetadataEntitySummary {
719 express_id: id,
720 type_name: type_name.to_string(),
721 name: format!("{type_name} #{id}"),
722 global_id: None,
723 kind: "element".to_string(),
724 has_children: false,
725 element_count: None,
726 elevation: None,
727 },
728 );
729 }
730 entity_jobs.push(EntityJob {
731 id,
732 ifc_type,
733 start,
734 end,
735 product_definition_shape_id: None,
736 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
737 global_id: None,
738 name: None,
739 presentation_layer: None,
740 space_zone_properties: None,
741 representation_map_id: None,
742 });
743 } else if ifc_lite_core::is_representationless_spatial_container_by_name(type_name)
744 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
745 {
746 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
758 entity_jobs.push(EntityJob {
759 id,
760 ifc_type,
761 start,
762 end,
763 product_definition_shape_id: None,
764 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
765 global_id: None,
766 name: None,
767 presentation_layer: None,
768 space_zone_properties: None,
769 representation_map_id: None,
770 });
771 }
772 else if type_name == "IFCMAPPEDITEM" {
779 let args = parse_step_arguments(&content[start..end]);
780 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
781 referenced_representation_maps.insert(source_id);
782 if options.enable_instancing {
786 mapped_item_plan
787 .entry(source_id)
788 .and_modify(|(count, template)| {
789 *count += 1;
790 if id < *template {
791 *template = id;
792 }
793 })
794 .or_insert((1, id));
795 }
796 }
797 } else if type_name == "IFCRELDEFINESBYTYPE" {
798 let args = parse_step_arguments(&content[start..end]);
801 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
802 instantiated_type_ids.insert(type_id);
803 }
804 prepass_spans.defines_by_type.push((id, start, end));
806 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
807 let args = parse_step_arguments(&content[start..end]);
808 let rep_map_ids = args
810 .get(6)
811 .map(|token| parse_step_ref_list(token))
812 .unwrap_or_default();
813 if !rep_map_ids.is_empty() {
814 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
815 }
816 }
817 }
818
819 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
821
822 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
830 for (rep_map_id, _class) in crate::element::plan_type_geometry(
835 rep_map_ids,
836 &referenced_representation_maps,
837 instantiated_type_ids.contains(type_id),
838 crate::element::TypeGeometryMode::SuppressInstanced,
839 ) {
840 entity_jobs.push(EntityJob {
841 id: *type_id,
842 ifc_type: *ifc_type,
843 start: *start,
844 end: *end,
845 product_definition_shape_id: None,
846 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
847 global_id: None,
848 name: None,
849 presentation_layer: None,
850 space_zone_properties: None,
851 representation_map_id: Some(rep_map_id),
852 });
853 }
854 }
855
856 let entity_index = match provided_index {
861 Some(idx) => ProcessingIndex::Hash(idx),
862 None => inline_index.finish(),
863 };
864 entity_index.install(&mut decoder);
865
866 let resolved = crate::prepass::resolve_prepass(
871 &prepass_spans,
872 &mut decoder,
873 crate::prepass::ResolveOptions {
874 collect_indexed_colour_full: true,
875 defer_attached_styles: defer_style_updates,
876 },
877 );
878 let crate::prepass::ResolvedPrepass {
879 mut geometry_style_index,
880 indexed_colour_index,
881 indexed_colour_full,
882 element_material_colors,
883 void_index,
884 filling_by_opening,
885 deferred_attached_styled_spans: deferred_styled_item_positions,
886 ..
887 } = resolved;
888
889 let entity_scan_time = entity_scan_start.elapsed();
890 scan_span.record("total_entities", total_entities as u64);
891 scan_span.record("geometry_entities", entity_jobs.len() as u64);
892 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
893 drop(scan_guard);
894
895 let lookup_start = Clock::now();
896 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
897 if options.include_properties {
898 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
899 }
900 if options.fast_first_batch {
901 entity_jobs.sort_by(|left, right| {
902 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
903 });
904 }
905 let lookup_time = lookup_start.elapsed();
906 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
907 drop(lookup_span);
908
909 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
910 &entity_jobs,
911 &void_index,
912 &filling_by_opening,
913 &geometry_style_index,
914 &mut decoder,
915 opening_filter,
916 );
917
918 if memchr::memmem::find(content, b"IFC4X3").is_some() {
922 schema_version = "IFC4X3".into();
923 } else if memchr::memmem::find(content, b"IFC4").is_some() {
924 schema_version = "IFC4".into();
925 }
926
927 let geometry_entity_count = entity_jobs.len();
928 tracing::info!(
929 total_entities = total_entities,
930 geometry_entities = geometry_entity_count,
931 voids = void_index.len(),
932 schema_version = %schema_version,
933 "Entity scanning complete"
934 );
935
936 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
937 for (parent_id, child_ids) in quick_aggregate_links {
938 if !spatial_nodes.contains_key(&parent_id) {
939 continue;
940 }
941 for child_id in child_ids {
942 if !spatial_nodes.contains_key(&child_id) {
943 continue;
944 }
945 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
946 parent.children.push(child_id);
947 }
948 if let Some(child) = spatial_nodes.get_mut(&child_id) {
949 child.parent = Some(parent_id);
950 }
951 }
952 }
953 for (parent_id, element_ids) in quick_containment_links {
954 if !spatial_nodes.contains_key(&parent_id) {
955 continue;
956 }
957 for child_id in element_ids {
958 if spatial_nodes.contains_key(&child_id) {
965 let already_placed = spatial_nodes
968 .get(&child_id)
969 .is_some_and(|child| child.parent.is_some());
970 if !already_placed {
971 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
972 parent.children.push(child_id);
973 }
974 if let Some(child) = spatial_nodes.get_mut(&child_id) {
975 child.parent = Some(parent_id);
976 }
977 }
978 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
979 parent.elements.push(child_id);
980 }
981 }
982 }
983 for (parent_id, element_ids) in quick_referenced_links {
987 if !spatial_nodes.contains_key(&parent_id) {
988 continue;
989 }
990 for child_id in element_ids {
991 if spatial_nodes.contains_key(&child_id) {
994 continue;
995 }
996 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
997 parent.elements.push(child_id);
998 }
999 }
1000 }
1001 let mut root_id = spatial_nodes
1002 .values()
1003 .find(|node| node.type_name == "IfcProject")
1004 .map(|node| node.express_id);
1005 if root_id.is_none() {
1006 root_id = spatial_nodes
1007 .values()
1008 .find(|node| node.parent.is_none())
1009 .map(|node| node.express_id);
1010 }
1011 let spatial_tree = root_id
1012 .map(|root| {
1013 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1014 })
1015 .transpose()
1016 .unwrap_or(None);
1017 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1018 schema_version: schema_version.clone(),
1019 entity_count: total_entities,
1020 spatial_tree,
1021 });
1022 }
1023
1024 let preprocess_start = Clock::now();
1026 let preprocess_span =
1027 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1028 let unit_scales = tracing::debug_span!("unit_scale")
1034 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1035 tracing::debug!(
1036 length_unit_scale = unit_scales.length_unit_scale,
1037 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1038 "Resolved unit scales"
1039 );
1040 decoder.seed_unit_scales(
1041 unit_scales.length_unit_scale,
1042 unit_scales.plane_angle_to_radians,
1043 );
1044 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1046 router.set_tessellation_quality(options.tessellation_quality);
1047 let material_spans = &prepass_spans.rel_associates_material;
1051 router.set_material_layer_index(Arc::new(
1052 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1053 ));
1054
1055 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1057 let entity = decoder.decode_at(start, end).ok()?;
1058 let matrix = router
1059 .resolve_scaled_placement(&entity, &mut decoder)
1060 .ok()?;
1061 Some(matrix.to_vec())
1062 });
1063 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1064 let entity = decoder.decode_at(start, end).ok()?;
1065 let matrix = router
1066 .resolve_scaled_placement(&entity, &mut decoder)
1067 .ok()?;
1068 Some(matrix.to_vec())
1069 });
1070
1071 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1072 .iter()
1073 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1074 .collect();
1075 let detected_rtc_offset =
1076 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1077
1078 let site_rtc = site_transform
1087 .as_ref()
1088 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1090 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1091 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1092 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1093 } else if detected_has_offset {
1094 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1095 } else {
1096 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1097 };
1098 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1099 router.set_rtc_offset(rtc_offset);
1100 let preprocess_time = preprocess_start.elapsed();
1101 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1102 drop(preprocess_span);
1103
1104 let parse_time = parse_start.elapsed();
1105 tracing::info!(
1106 entity_scan_time_ms = entity_scan_time.as_millis(),
1107 lookup_time_ms = lookup_time.as_millis(),
1108 preprocess_time_ms = preprocess_time.as_millis(),
1109 parse_time_ms = parse_time.as_millis(),
1110 "Parse phase complete, starting geometry extraction"
1111 );
1112
1113 let geometry_start = Clock::now();
1115 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1117 let rtc_offset = router.rtc_offset();
1118 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1126 let void_index_arc = Arc::new(filtered_void_index);
1127 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1128 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1131 let mut geometry_style_index = Arc::new(geometry_style_index);
1132 let indexed_colour_full = Arc::new(indexed_colour_full);
1133 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1138 content,
1139 &mut decoder,
1140 ));
1141 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1145 .iter()
1146 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1147 .collect();
1148 let element_material_colors = Arc::new(element_material_colors);
1149
1150 let total_jobs = entity_jobs.len();
1151 let initial_chunk_size = options.initial_batch_size.max(1);
1152 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1153 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1154 FxHashMap::default();
1155 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1156 FxHashMap::default();
1157 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1158 let mut meshes: Vec<MeshData> = Vec::new();
1159 let mut processed_jobs = 0usize;
1160 let mut total_meshes = 0usize;
1161 let mut total_vertices = 0usize;
1162 let mut total_triangles = 0usize;
1163 let mut chunk_start = 0usize;
1164 let mut current_chunk_size = initial_chunk_size;
1165
1166 let mut deferred_styles_applied = !defer_style_updates;
1167
1168 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1171
1172 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1177 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1178
1179 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1183
1184 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1204 && options.retain_emitted_meshes
1205 && coord_space != SITE_LOCAL_MESH_COORDINATE_SPACE)
1206 .then(|| {
1207 Arc::new(
1208 mapped_item_plan
1209 .into_iter()
1210 .filter(|(_, (count, _))| *count >= 2)
1211 .collect::<FxHashMap<u32, (u32, u32)>>(),
1212 )
1213 });
1214 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1233 && !indexed_colour_full.is_empty())
1234 .then(|| {
1235 let ids: FxHashSet<u32> = indexed_colour_full
1236 .iter()
1237 .filter(|(_, m)| m.has_multiple_colours())
1238 .map(|(&id, _)| id)
1239 .collect();
1240 (!ids.is_empty()).then(|| Arc::new(ids))
1241 })
1242 .flatten();
1243 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1246 std::sync::Mutex::new(Vec::new());
1247
1248 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1256 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1257 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1258
1259 let worker_point_caches = jobs::new_worker_point_caches();
1260 let worker_placement_caches = jobs::new_worker_placement_caches();
1264
1265 let geometry_span = tracing::info_span!(
1266 "geometry",
1267 element_count = total_jobs,
1268 mesh_count = tracing::field::Empty,
1269 triangle_count = tracing::field::Empty,
1270 backstop_count = tracing::field::Empty,
1271 total_csg_failures = tracing::field::Empty,
1272 phase_ms = tracing::field::Empty,
1273 );
1274 let geometry_guard = geometry_span.clone().entered();
1275
1276 while chunk_start < total_jobs {
1277 if options
1281 .cancel
1282 .as_ref()
1283 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1284 {
1285 break;
1286 }
1287 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1288 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1289
1290 #[cfg(not(target_arch = "wasm32"))]
1294 {
1295 let entity_index_for_meta = entity_index_arc.clone();
1297 jobs_chunk.par_iter_mut().for_each(|job| {
1298 if job.global_id.is_some()
1299 || job.name.is_some()
1300 || job.product_definition_shape_id.is_some()
1301 {
1302 return;
1303 }
1304 let mut local_decoder =
1305 entity_index_for_meta.decoder(content);
1306 let Ok(entity) = local_decoder.decode_at(job.start, job.end) else {
1307 return;
1308 };
1309 job.global_id = normalize_optional_string(entity.get_string(0));
1310 job.name = normalize_optional_string(entity.get_string(2));
1311 job.product_definition_shape_id = entity.get_ref(6);
1312 });
1313
1314 for job in jobs_chunk.iter_mut() {
1316 let Some(pds_id) = job.product_definition_shape_id else {
1317 continue;
1318 };
1319 let resolved_color = color_cache_by_product_definition_shape
1320 .entry(pds_id)
1321 .or_insert_with(|| {
1322 resolve_element_color_for_product_definition_shape(
1323 pds_id,
1324 &geometry_style_index,
1325 &mut decoder,
1326 )
1327 });
1328 if let Some(color) = resolved_color {
1329 job.element_color = *color;
1330 } else if let Some(color) = element_material_color.get(&job.id) {
1331 job.element_color = *color;
1334 }
1335 if options.include_presentation_layers {
1336 let resolved_layer = layer_cache_by_product_definition_shape
1337 .entry(pds_id)
1338 .or_insert_with(|| {
1339 resolve_presentation_layer_for_product_definition_shape(
1340 pds_id,
1341 &presentation_layer_by_assigned_id,
1342 &mut layer_cache_by_representation,
1343 &mut decoder,
1344 )
1345 });
1346 job.presentation_layer = resolved_layer.clone();
1347 }
1348 }
1349 }
1350
1351 #[cfg(target_arch = "wasm32")]
1353 for job in jobs_chunk.iter_mut() {
1354 populate_entity_job_metadata(
1355 job,
1356 &geometry_style_index,
1357 &element_material_color,
1358 &presentation_layer_by_assigned_id,
1359 &mut color_cache_by_product_definition_shape,
1360 &mut layer_cache_by_product_definition_shape,
1361 &mut layer_cache_by_representation,
1362 &mut decoder,
1363 options.include_presentation_layers,
1364 );
1365 }
1366 let site_local_rotation: Option<&Vec<f64>> =
1367 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1368 site_transform.as_ref()
1369 } else {
1370 None
1371 };
1372 let chunk_meshes: Vec<MeshData> = jobs_chunk
1377 .par_iter()
1378 .map(|job| {
1379 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1380 let mut fallback_cache = FxHashMap::default();
1390 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1391 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1392 match slot_guard.as_deref_mut() {
1393 Some(cache) => cache,
1394 None => &mut fallback_cache,
1395 };
1396 let mut fallback_placement_cache = FxHashMap::default();
1405 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1406 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1407 match placement_slot_guard.as_deref_mut() {
1408 Some(cache) => cache,
1409 None => &mut fallback_placement_cache,
1410 };
1411 process_entity_job(
1412 job,
1413 content,
1414 &entity_index_arc,
1415 unit_scale,
1416 rtc_offset,
1417 seed_plane_angle_to_radians,
1418 options.tessellation_quality,
1419 void_index_arc.as_ref(),
1420 skipped_entity_ids.as_ref(),
1421 geometry_style_index.as_ref(),
1422 indexed_colour_full.as_ref(),
1423 element_material_colors.as_ref(),
1424 texture_index.as_ref(),
1425 site_local_rotation,
1426 &diag_collectors,
1427 &item_dedup_cache,
1428 &brep_signature_cache,
1429 &mapped_item_cache,
1430 instancing_plan.as_ref(),
1431 indexed_colour_split_ids.as_ref(),
1432 &raw_instance_collector,
1433 worker_point_cache,
1434 worker_placement_cache,
1435 &point_cache_hits_collector,
1436 &point_cache_misses_collector,
1437 &faceted_brep_ns_collector,
1438 )
1439 })
1440 .flatten_iter()
1441 .collect();
1442
1443 processed_jobs += jobs_chunk.len();
1444 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1445 total_triangles += chunk_meshes
1446 .iter()
1447 .map(|m| m.triangle_count())
1448 .sum::<usize>();
1449
1450 if !chunk_meshes.is_empty() {
1451 total_meshes += chunk_meshes.len();
1452 let emit_mesh_chunk_size = current_chunk_size.max(1);
1453 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1454 on_batch(emitted_meshes, processed_jobs, total_jobs);
1455 }
1456 if options.retain_emitted_meshes {
1457 meshes.extend(chunk_meshes);
1458 }
1459
1460 if !deferred_styles_applied {
1461 let mut rebuilt_styles = {
1466 let mut style_decoder =
1467 entity_index_arc.decoder(content);
1468 crate::prepass::resolve_styled_item_spans(
1469 &deferred_styled_item_positions,
1470 &mut style_decoder,
1471 )
1472 };
1473 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1474 geometry_style_index = Arc::new(rebuilt_styles);
1475 let deferred_color_updates = build_color_updates_for_jobs(
1476 &entity_jobs[..processed_jobs],
1477 geometry_style_index.as_ref(),
1478 content,
1479 &entity_index_arc,
1480 );
1481 if !deferred_color_updates.is_empty() {
1482 on_color_update(&deferred_color_updates);
1483 }
1484 deferred_styles_applied = true;
1485 }
1486 }
1487 chunk_start = chunk_end;
1488 current_chunk_size = throughput_chunk_size;
1489 }
1490
1491 let geometry_time = geometry_start.elapsed();
1492 let csg_failures = diag_collectors
1495 .csg_failures
1496 .into_inner()
1497 .unwrap_or_else(|poisoned| poisoned.into_inner());
1498 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1499 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1500 let backstop_dropped = diag_collectors.backstop.into_inner();
1501 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1503 let point_cache_hits = point_cache_hits_collector.into_inner();
1504 let point_cache_misses = point_cache_misses_collector.into_inner();
1505 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1506 geometry_span.record("mesh_count", total_meshes as u64);
1507 geometry_span.record("triangle_count", total_triangles as u64);
1508 geometry_span.record("backstop_count", backstop_dropped);
1509 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1510 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1511 drop(geometry_guard);
1512 if total_csg_failures > 0 {
1513 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1514 for fails in csg_failures.values() {
1515 for f in fails {
1516 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1517 }
1518 }
1519 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1520 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1521 let breakdown = breakdown
1522 .iter()
1523 .map(|(reason, count)| format!("{reason}={count}"))
1524 .collect::<Vec<_>>()
1525 .join(" ");
1526 tracing::warn!(
1527 total_csg_failures,
1528 products_with_failures,
1529 %breakdown,
1530 "CSG failures during geometry extraction (cut dropped, host kept uncut)"
1531 );
1532 }
1533
1534 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1535 diagnostics::collate(
1536 diag_collectors.classification,
1537 diag_collectors.host_diags,
1538 diag_collectors.rect_fast,
1539 diag_collectors.unsupported_items,
1540 &csg_failures,
1541 oversized_ref_drops,
1542 )
1543 });
1544
1545 let instances = instancing::finalize_instances(
1550 raw_instance_collector
1551 .into_inner()
1552 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1553 &mut meshes,
1554 &mapped_item_cache,
1555 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1556 );
1557
1558 let total_time = total_start.elapsed();
1559 pipeline_span.record("element_count", total_jobs as u64);
1560 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1561
1562 tracing::info!(
1563 meshes = meshes.len(),
1564 instances = instances.len(),
1565 vertices = total_vertices,
1566 triangles = total_triangles,
1567 backstop_count = backstop_dropped,
1568 geometry_time_ms = geometry_time.as_millis(),
1569 total_time_ms = total_time.as_millis(),
1570 "Geometry processing complete"
1571 );
1572
1573 ProcessingResult {
1574 meshes,
1575 instances,
1576 mesh_coordinate_space: Some(coord_space.to_string()),
1577 site_transform,
1578 building_transform,
1579 metadata: ModelMetadata {
1580 schema_version,
1581 entity_count: total_entities,
1582 geometry_entity_count,
1583 coordinate_info: CoordinateInfo {
1584 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1585 is_geo_referenced: has_rtc_offset,
1586 },
1587 length_unit_scale: Some(unit_scale),
1588 georeferencing: crate::georeferencing::extract_georeferencing_from_candidates(
1589 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1590 ),
1591 },
1592 stats: ProcessingStats {
1593 total_meshes,
1594 total_vertices,
1595 total_triangles,
1596 parse_time_ms: parse_time.as_millis() as u64,
1597 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1598 lookup_time_ms: lookup_time.as_millis() as u64,
1599 preprocess_time_ms: preprocess_time.as_millis() as u64,
1600 geometry_time_ms: geometry_time.as_millis() as u64,
1601 total_time_ms: total_time.as_millis() as u64,
1602 from_cache: false,
1603 total_csg_failures: total_csg_failures as u64,
1604 products_with_failures,
1605 degenerate_triangles_dropped: backstop_dropped,
1606 point_cache_hits,
1607 point_cache_misses,
1608 faceted_brep_time_ms,
1609 geometry_diagnostics,
1610 },
1611 }
1612}
1613
1614