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;
27pub(crate) mod instancing;
28mod jobs;
29mod opening_filter;
30mod properties;
31mod quick_metadata;
32mod site_local;
33
34pub use quick_metadata::is_quick_spatial_type_ci;
35pub use site_local::convert_mesh_to_site_local;
36
37use jobs::{build_color_updates_for_jobs, process_entity_job};
38
39use color_layer::{
40 collect_presentation_layer_assignments, resolve_element_color_for_product_definition_shape,
41 resolve_presentation_layer_for_product_definition_shape,
42};
43use opening_filter::apply_opening_filter;
44use properties::resolve_space_zone_properties_lazy;
45use quick_metadata::{
46 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
47 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
48};
49use site_local::{
50 translation_is_nonidentity, MODEL_RTC_MESH_COORDINATE_SPACE, RAW_IFC_MESH_COORDINATE_SPACE,
51 SITE_LOCAL_MESH_COORDINATE_SPACE,
52};
53
54#[cfg(not(target_arch = "wasm32"))]
61use std::time::Instant as Clock;
62
63#[cfg(target_arch = "wasm32")]
64#[derive(Clone, Copy)]
65struct Clock;
66
67#[cfg(target_arch = "wasm32")]
68impl Clock {
69 #[inline]
70 fn now() -> Self {
71 Clock
72 }
73 #[inline]
74 fn elapsed(&self) -> std::time::Duration {
75 std::time::Duration::ZERO
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum OpeningFilterMode {
83 #[default]
85 Default = 0,
86 IgnoreAll = 1,
88 IgnoreOpaque = 2,
90}
91
92impl OpeningFilterMode {
93 pub fn cache_key_suffix(&self) -> &'static str {
96 match self {
97 Self::Default => "default",
98 Self::IgnoreAll => "ignore_all",
99 Self::IgnoreOpaque => "ignore_opaque",
100 }
101 }
102}
103
104pub struct ProcessingResult {
106 pub meshes: Vec<MeshData>,
107 pub instances: Vec<InstanceRecord>,
114 pub mesh_coordinate_space: Option<String>,
116 pub site_transform: Option<Vec<f64>>,
118 pub building_transform: Option<Vec<f64>>,
120 pub metadata: ModelMetadata,
121 pub stats: ProcessingStats,
122}
123
124#[derive(Debug, Clone)]
128pub struct StreamingOptions {
129 pub initial_batch_size: usize,
131 pub throughput_batch_size: usize,
133 pub fast_first_batch: bool,
135 pub include_properties: bool,
137 pub include_presentation_layers: bool,
139 pub emit_quick_metadata_bootstrap: bool,
141 pub retain_emitted_meshes: bool,
143 pub tessellation_quality: TessellationQuality,
150 pub entity_index: Option<Arc<EntityIndex>>,
158 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
164 pub enable_instancing: bool,
173}
174
175impl Default for StreamingOptions {
176 fn default() -> Self {
177 Self {
178 initial_batch_size: 50,
179 throughput_batch_size: 50,
180 fast_first_batch: false,
181 include_properties: true,
182 include_presentation_layers: true,
183 emit_quick_metadata_bootstrap: false,
184 retain_emitted_meshes: true,
185 tessellation_quality: TessellationQuality::default(),
186 entity_index: None,
187 cancel: None,
188 enable_instancing: false,
189 }
190 }
191}
192
193pub(super) struct EntityJob {
195 pub(super) id: u32,
196 pub(super) ifc_type: IfcType,
197 pub(super) start: usize,
198 pub(super) end: usize,
199 pub(super) product_definition_shape_id: Option<u32>,
200 pub(super) element_color: [f32; 4],
201 pub(super) global_id: Option<String>,
202 pub(super) name: Option<String>,
203 pub(super) presentation_layer: Option<String>,
204 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
205 pub(super) representation_map_id: Option<u32>,
209}
210
211#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
213#[allow(clippy::too_many_arguments)]
215fn populate_entity_job_metadata(
216 job: &mut EntityJob,
217 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
218 element_material_color: &FxHashMap<u32, [f32; 4]>,
219 layer_by_assigned_representation: &FxHashMap<u32, String>,
220 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
221 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
222 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
223 decoder: &mut EntityDecoder,
224 include_presentation_layers: bool,
225) {
226 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
227 return;
228 }
229
230 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
231 return;
232 };
233
234 job.global_id = normalize_optional_string(entity.get_string(0));
235 job.name = normalize_optional_string(entity.get_string(2));
236 job.product_definition_shape_id = entity.get_ref(6);
237
238 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
239 return;
240 };
241
242 let resolved_color = color_cache_by_product_definition_shape
243 .entry(product_definition_shape_id)
244 .or_insert_with(|| {
245 resolve_element_color_for_product_definition_shape(
246 product_definition_shape_id,
247 geometry_style_index,
248 decoder,
249 )
250 });
251 if let Some(color) = resolved_color {
252 job.element_color = *color;
253 } else if let Some(color) = element_material_color.get(&job.id) {
254 job.element_color = *color;
255 }
256
257 if include_presentation_layers {
258 let resolved_layer = layer_cache_by_product_definition_shape
259 .entry(product_definition_shape_id)
260 .or_insert_with(|| {
261 resolve_presentation_layer_for_product_definition_shape(
262 product_definition_shape_id,
263 layer_by_assigned_representation,
264 layer_cache_by_representation,
265 decoder,
266 )
267 });
268 job.presentation_layer = resolved_layer.clone();
269 }
270}
271
272use crate::style::GeometryStyleInfo;
276
277pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
279 let list = entity.get_list(index)?;
280 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
281 if refs.is_empty() {
282 None
283 } else {
284 Some(refs)
285 }
286}
287
288pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
289 let value = raw?.trim();
290 if value.is_empty() || value == "$" {
291 return None;
292 }
293 Some(value.to_string())
294}
295
296fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
297 match ifc_type {
298 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
299 IfcType::IfcSlab => 95,
300 IfcType::IfcColumn => 90,
301 IfcType::IfcBeam => 85,
302 IfcType::IfcRoof => 80,
303 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
304 IfcType::IfcCurtainWall => 70,
305 IfcType::IfcFooting | IfcType::IfcPile => 65,
306 IfcType::IfcDoor | IfcType::IfcWindow => 30,
307 IfcType::IfcFurnishingElement => 10,
308 _ => 50,
309 }
310}
311
312pub fn process_geometry<T>(content: &T) -> ProcessingResult
314where
315 T: AsRef<[u8]> + ?Sized,
316{
317 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
318}
319
320pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
327where
328 T: AsRef<[u8]> + ?Sized,
329{
330 process_geometry_streaming_filtered_with_options(
331 content.as_ref(),
332 OpeningFilterMode::Default,
333 StreamingOptions {
334 initial_batch_size: usize::MAX,
335 throughput_batch_size: usize::MAX,
336 entity_index: Some(index),
337 ..StreamingOptions::default()
338 },
339 |_, _, _| {},
340 |_| {},
341 |_| {},
342 )
343}
344
345pub fn process_geometry_streaming(
347 content: &[u8],
348 batch_size: usize,
349 on_batch: impl FnMut(&[MeshData], usize, usize),
350) -> ProcessingResult {
351 process_geometry_streaming_with_options(
352 content,
353 StreamingOptions {
354 initial_batch_size: batch_size,
355 throughput_batch_size: batch_size,
356 ..StreamingOptions::default()
357 },
358 on_batch,
359 |_| {},
360 )
361}
362
363pub fn process_geometry_streaming_with_options(
365 content: &[u8],
366 options: StreamingOptions,
367 on_batch: impl FnMut(&[MeshData], usize, usize),
368 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
369) -> ProcessingResult {
370 process_geometry_streaming_with_options_and_bootstrap(
371 content,
372 options,
373 on_batch,
374 on_color_update,
375 |_| {},
376 )
377}
378
379pub fn process_geometry_streaming_with_options_and_bootstrap(
382 content: &[u8],
383 options: StreamingOptions,
384 on_batch: impl FnMut(&[MeshData], usize, usize),
385 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
386 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
387) -> ProcessingResult {
388 process_geometry_streaming_filtered_with_options(
389 content,
390 OpeningFilterMode::Default,
391 options,
392 on_batch,
393 on_color_update,
394 on_quick_metadata_bootstrap,
395 )
396}
397
398pub fn process_geometry_filtered<T>(
400 content: &T,
401 opening_filter: OpeningFilterMode,
402) -> ProcessingResult
403where
404 T: AsRef<[u8]> + ?Sized,
405{
406 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
407}
408
409pub fn process_geometry_filtered_with_quality<T>(
413 content: &T,
414 opening_filter: OpeningFilterMode,
415 tessellation_quality: TessellationQuality,
416) -> ProcessingResult
417where
418 T: AsRef<[u8]> + ?Sized,
419{
420 let content = content.as_ref();
421 process_geometry_streaming_filtered_with_options(
422 content,
423 opening_filter,
424 StreamingOptions {
425 initial_batch_size: usize::MAX,
426 throughput_batch_size: usize::MAX,
427 tessellation_quality,
428 ..StreamingOptions::default()
429 },
430 |_, _, _| {},
431 |_| {},
432 |_| {},
433 )
434}
435
436pub fn process_geometry_streaming_filtered(
438 content: &[u8],
439 opening_filter: OpeningFilterMode,
440 batch_size: usize,
441 on_batch: impl FnMut(&[MeshData], usize, usize),
442 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
443) -> ProcessingResult {
444 process_geometry_streaming_filtered_with_options(
445 content,
446 opening_filter,
447 StreamingOptions {
448 initial_batch_size: batch_size,
449 throughput_batch_size: batch_size,
450 ..StreamingOptions::default()
451 },
452 on_batch,
453 on_color_update,
454 |_| {},
455 )
456}
457
458pub fn process_geometry_streaming_filtered_with_options(
460 content: &[u8],
461 opening_filter: OpeningFilterMode,
462 options: StreamingOptions,
463 mut on_batch: impl FnMut(&[MeshData], usize, usize),
464 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
465 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
466) -> ProcessingResult {
467 let total_start = Clock::now();
468 let parse_start = Clock::now();
469 let entity_scan_start = Clock::now();
470
471 let pipeline_span = tracing::info_span!(
478 "geometry_pipeline",
479 byte_size = content.len(),
480 element_count = tracing::field::Empty,
481 total_ms = tracing::field::Empty,
482 );
483 let _pipeline_guard = pipeline_span.clone().entered();
484
485 tracing::info!(
486 content_size = content.len(),
487 "Starting IFC geometry processing"
488 );
489
490 let scan_span = tracing::info_span!(
491 "scan_prepass",
492 total_entities = tracing::field::Empty,
493 geometry_entities = tracing::field::Empty,
494 phase_ms = tracing::field::Empty,
495 );
496 let scan_guard = scan_span.clone().entered();
497
498 let provided_index = options.entity_index.clone();
505 let building_index = provided_index.is_none();
506 let mut inline_index: EntityIndex = if building_index {
507 FxHashMap::with_capacity_and_hasher(content.len() / 50, Default::default())
508 } else {
509 FxHashMap::default()
510 };
511 let mut decoder = match &provided_index {
512 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
513 None => EntityDecoder::new(content),
514 };
515 tracing::debug!("Entity index will be built inline during the scan");
516
517 let mut prepass_spans = crate::prepass::PrepassSpans::default();
523 let mut project_id: Option<u32> = None;
524 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
525 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
532
533 let mut scanner = EntityScanner::new(content);
535 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
536 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
541 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
542 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
547 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
556 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
557 let mut quick_spatial_nodes =
558 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
559 let mut quick_aggregate_links = if quick_metadata_enabled {
560 Vec::<(u32, Vec<u32>)>::new()
561 } else {
562 Vec::new()
563 };
564 let mut quick_containment_links = if quick_metadata_enabled {
565 Vec::<(u32, Vec<u32>)>::new()
566 } else {
567 Vec::new()
568 };
569 let mut quick_referenced_links = if quick_metadata_enabled {
574 Vec::<(u32, Vec<u32>)>::new()
575 } else {
576 Vec::new()
577 };
578 let mut quick_element_summaries = if quick_metadata_enabled {
579 HashMap::<u32, QuickMetadataEntitySummary>::new()
580 } else {
581 HashMap::new()
582 };
583 let mut schema_version = "IFC2X3".to_string();
584 let mut total_entities = 0usize;
585 let mut site_entity_pos: Option<(usize, usize)> = None;
586 let mut building_entity_pos: Option<(usize, usize)> = None;
587
588 let defer_style_updates = options.fast_first_batch
589 && opening_filter == OpeningFilterMode::Default
590 && !options.include_presentation_layers;
591
592 while let Some((id, type_name, start, end)) = scanner.next_entity() {
593 total_entities += 1;
594 if building_index {
595 inline_index.insert(id, (start, end));
596 }
597 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
598 if is_quick_spatial_type_ci(type_name) {
600 let args = parse_step_arguments(&content[start..end]);
601 let fallback = format!("{type_name} #{id}");
602 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
603 express_id: id,
604 type_name: type_name.to_string(),
605 name: extract_name_from_args(&args, &fallback),
606 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
607 extract_storey_elevation_from_args(&args)
608 } else {
609 None
610 },
611 children: Vec::new(),
612 elements: Vec::new(),
613 parent: None,
614 });
615 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
616 let args = parse_step_arguments(&content[start..end]);
617 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
618 quick_aggregate_links.push((
619 parent_id,
620 args.get(5)
621 .map(|token| parse_step_ref_list(token))
622 .unwrap_or_default(),
623 ));
624 }
625 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
626 let args = parse_step_arguments(&content[start..end]);
627 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
628 quick_containment_links.push((
629 parent_id,
630 args.get(4)
631 .map(|token| parse_step_ref_list(token))
632 .unwrap_or_default(),
633 ));
634 }
635 } else if type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
636 let args = parse_step_arguments(&content[start..end]);
637 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
638 quick_referenced_links.push((
639 parent_id,
640 args.get(4)
641 .map(|token| parse_step_ref_list(token))
642 .unwrap_or_default(),
643 ));
644 }
645 }
646 }
647
648 if type_name == "IFCINDEXEDCOLOURMAP" {
649 prepass_spans.indexed_colour_maps.push((id, start, end));
651 continue;
652 }
653
654 if type_name == "IFCSTYLEDITEM" {
655 prepass_spans.styled_items.push((id, start, end));
659 continue;
660 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
661 prepass_spans.material_def_reprs.push((id, start, end));
662 continue;
663 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
664 prepass_spans.rel_associates_material.push((id, start, end));
665 continue;
666 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
667 if !options.include_presentation_layers {
668 continue;
669 }
670 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
671 collect_presentation_layer_assignments(
672 &mut presentation_layer_by_assigned_id,
673 &layer_assignment,
674 );
675 }
676 continue;
677 } else if type_name == "IFCPROPERTYSET" {
678 continue;
681 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
682 if options.include_properties {
683 rel_defines_spans.push((start, end));
684 }
685 continue;
686 } else if type_name.starts_with("IFCPROPERTY") {
687 continue;
690 } else if type_name == "IFCRELVOIDSELEMENT" {
691 prepass_spans.void_rels.push((id, start, end));
692 } else if type_name == "IFCRELFILLSELEMENT" {
693 prepass_spans.fills_rels.push((id, start, end));
694 } else if type_name == "IFCRELAGGREGATES" {
695 prepass_spans.aggregate_rels.push((id, start, end));
700 } else if type_name == "IFCPROJECT" && project_id.is_none() {
701 project_id = Some(id);
702 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
703 site_entity_pos = Some((start, end));
704 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
705 building_entity_pos = Some((start, end));
706 }
707
708 if ifc_lite_core::has_geometry_by_name(type_name) {
709 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
713 if quick_metadata_enabled {
714 quick_element_summaries.insert(
715 id,
716 QuickMetadataEntitySummary {
717 express_id: id,
718 type_name: type_name.to_string(),
719 name: format!("{type_name} #{id}"),
720 global_id: None,
721 kind: "element".to_string(),
722 has_children: false,
723 element_count: None,
724 elevation: None,
725 },
726 );
727 }
728 entity_jobs.push(EntityJob {
729 id,
730 ifc_type,
731 start,
732 end,
733 product_definition_shape_id: None,
734 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
735 global_id: None,
736 name: None,
737 presentation_layer: None,
738 space_zone_properties: None,
739 representation_map_id: None,
740 });
741 } else if ifc_lite_core::is_representationless_spatial_container_by_name(type_name)
742 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
743 {
744 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
756 entity_jobs.push(EntityJob {
757 id,
758 ifc_type,
759 start,
760 end,
761 product_definition_shape_id: None,
762 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
763 global_id: None,
764 name: None,
765 presentation_layer: None,
766 space_zone_properties: None,
767 representation_map_id: None,
768 });
769 }
770 else if type_name == "IFCMAPPEDITEM" {
777 let args = parse_step_arguments(&content[start..end]);
778 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
779 referenced_representation_maps.insert(source_id);
780 if options.enable_instancing {
784 mapped_item_plan
785 .entry(source_id)
786 .and_modify(|(count, template)| {
787 *count += 1;
788 if id < *template {
789 *template = id;
790 }
791 })
792 .or_insert((1, id));
793 }
794 }
795 } else if type_name == "IFCRELDEFINESBYTYPE" {
796 let args = parse_step_arguments(&content[start..end]);
799 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
800 instantiated_type_ids.insert(type_id);
801 }
802 prepass_spans.defines_by_type.push((id, start, end));
804 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
805 let args = parse_step_arguments(&content[start..end]);
806 let rep_map_ids = args
808 .get(6)
809 .map(|token| parse_step_ref_list(token))
810 .unwrap_or_default();
811 if !rep_map_ids.is_empty() {
812 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
813 }
814 }
815 }
816
817 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
819
820 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
828 for (rep_map_id, _class) in crate::element::plan_type_geometry(
833 rep_map_ids,
834 &referenced_representation_maps,
835 instantiated_type_ids.contains(type_id),
836 crate::element::TypeGeometryMode::SuppressInstanced,
837 ) {
838 entity_jobs.push(EntityJob {
839 id: *type_id,
840 ifc_type: *ifc_type,
841 start: *start,
842 end: *end,
843 product_definition_shape_id: None,
844 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
845 global_id: None,
846 name: None,
847 presentation_layer: None,
848 space_zone_properties: None,
849 representation_map_id: Some(rep_map_id),
850 });
851 }
852 }
853
854 let entity_index: Arc<EntityIndex> = match provided_index {
859 Some(idx) => idx,
860 None => {
861 let arc = Arc::new(inline_index);
862 decoder.set_entity_index(arc.clone());
863 arc
864 }
865 };
866
867 let resolved = crate::prepass::resolve_prepass(
872 &prepass_spans,
873 &mut decoder,
874 crate::prepass::ResolveOptions {
875 collect_indexed_colour_full: true,
876 defer_attached_styles: defer_style_updates,
877 },
878 );
879 let crate::prepass::ResolvedPrepass {
880 mut geometry_style_index,
881 indexed_colour_index,
882 indexed_colour_full,
883 element_material_colors,
884 void_index,
885 filling_by_opening,
886 deferred_attached_styled_spans: deferred_styled_item_positions,
887 ..
888 } = resolved;
889
890 let entity_scan_time = entity_scan_start.elapsed();
891 scan_span.record("total_entities", total_entities as u64);
892 scan_span.record("geometry_entities", entity_jobs.len() as u64);
893 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
894 drop(scan_guard);
895
896 let lookup_start = Clock::now();
897 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
898 if options.include_properties {
899 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
900 }
901 if options.fast_first_batch {
902 entity_jobs.sort_by(|left, right| {
903 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
904 });
905 }
906 let lookup_time = lookup_start.elapsed();
907 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
908 drop(lookup_span);
909
910 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
911 &entity_jobs,
912 &void_index,
913 &filling_by_opening,
914 &geometry_style_index,
915 &mut decoder,
916 opening_filter,
917 );
918
919 if memchr::memmem::find(content, b"IFC4X3").is_some() {
923 schema_version = "IFC4X3".into();
924 } else if memchr::memmem::find(content, b"IFC4").is_some() {
925 schema_version = "IFC4".into();
926 }
927
928 let geometry_entity_count = entity_jobs.len();
929 tracing::info!(
930 total_entities = total_entities,
931 geometry_entities = geometry_entity_count,
932 voids = void_index.len(),
933 schema_version = %schema_version,
934 "Entity scanning complete"
935 );
936
937 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
938 for (parent_id, child_ids) in quick_aggregate_links {
939 if !spatial_nodes.contains_key(&parent_id) {
940 continue;
941 }
942 for child_id in child_ids {
943 if !spatial_nodes.contains_key(&child_id) {
944 continue;
945 }
946 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
947 parent.children.push(child_id);
948 }
949 if let Some(child) = spatial_nodes.get_mut(&child_id) {
950 child.parent = Some(parent_id);
951 }
952 }
953 }
954 for (parent_id, element_ids) in quick_containment_links {
955 if !spatial_nodes.contains_key(&parent_id) {
956 continue;
957 }
958 for child_id in element_ids {
959 if spatial_nodes.contains_key(&child_id) {
966 let already_placed = spatial_nodes
969 .get(&child_id)
970 .is_some_and(|child| child.parent.is_some());
971 if !already_placed {
972 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
973 parent.children.push(child_id);
974 }
975 if let Some(child) = spatial_nodes.get_mut(&child_id) {
976 child.parent = Some(parent_id);
977 }
978 }
979 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
980 parent.elements.push(child_id);
981 }
982 }
983 }
984 for (parent_id, element_ids) in quick_referenced_links {
988 if !spatial_nodes.contains_key(&parent_id) {
989 continue;
990 }
991 for child_id in element_ids {
992 if spatial_nodes.contains_key(&child_id) {
995 continue;
996 }
997 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
998 parent.elements.push(child_id);
999 }
1000 }
1001 }
1002 let mut root_id = spatial_nodes
1003 .values()
1004 .find(|node| node.type_name == "IfcProject")
1005 .map(|node| node.express_id);
1006 if root_id.is_none() {
1007 root_id = spatial_nodes
1008 .values()
1009 .find(|node| node.parent.is_none())
1010 .map(|node| node.express_id);
1011 }
1012 let spatial_tree = root_id
1013 .map(|root| {
1014 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1015 })
1016 .transpose()
1017 .unwrap_or(None);
1018 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1019 schema_version: schema_version.clone(),
1020 entity_count: total_entities,
1021 spatial_tree,
1022 });
1023 }
1024
1025 let preprocess_start = Clock::now();
1027 let preprocess_span =
1028 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1029 let unit_scales = tracing::debug_span!("unit_scale")
1035 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1036 tracing::debug!(
1037 length_unit_scale = unit_scales.length_unit_scale,
1038 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1039 "Resolved unit scales"
1040 );
1041 decoder.seed_unit_scales(
1042 unit_scales.length_unit_scale,
1043 unit_scales.plane_angle_to_radians,
1044 );
1045 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1047 router.set_tessellation_quality(options.tessellation_quality);
1048 let material_spans = &prepass_spans.rel_associates_material;
1052 router.set_material_layer_index(Arc::new(
1053 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1054 ));
1055
1056 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1058 let entity = decoder.decode_at(start, end).ok()?;
1059 let matrix = router
1060 .resolve_scaled_placement(&entity, &mut decoder)
1061 .ok()?;
1062 Some(matrix.to_vec())
1063 });
1064 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1065 let entity = decoder.decode_at(start, end).ok()?;
1066 let matrix = router
1067 .resolve_scaled_placement(&entity, &mut decoder)
1068 .ok()?;
1069 Some(matrix.to_vec())
1070 });
1071
1072 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1073 .iter()
1074 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1075 .collect();
1076 let detected_rtc_offset =
1077 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1078
1079 let site_rtc = site_transform
1088 .as_ref()
1089 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1091 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1092 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1093 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1094 } else if detected_has_offset {
1095 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1096 } else {
1097 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1098 };
1099 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1100 router.set_rtc_offset(rtc_offset);
1101 let preprocess_time = preprocess_start.elapsed();
1102 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1103 drop(preprocess_span);
1104
1105 let parse_time = parse_start.elapsed();
1106 tracing::info!(
1107 entity_scan_time_ms = entity_scan_time.as_millis(),
1108 lookup_time_ms = lookup_time.as_millis(),
1109 preprocess_time_ms = preprocess_time.as_millis(),
1110 parse_time_ms = parse_time.as_millis(),
1111 "Parse phase complete, starting geometry extraction"
1112 );
1113
1114 let geometry_start = Clock::now();
1116 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1118 let rtc_offset = router.rtc_offset();
1119 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1127 let void_index_arc = Arc::new(filtered_void_index);
1128 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1129 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1132 let mut geometry_style_index = Arc::new(geometry_style_index);
1133 let indexed_colour_full = Arc::new(indexed_colour_full);
1134 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1139 content,
1140 &mut decoder,
1141 ));
1142 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1146 .iter()
1147 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1148 .collect();
1149 let element_material_colors = Arc::new(element_material_colors);
1150
1151 let total_jobs = entity_jobs.len();
1152 let initial_chunk_size = options.initial_batch_size.max(1);
1153 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1154 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1155 FxHashMap::default();
1156 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1157 FxHashMap::default();
1158 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1159 let mut meshes: Vec<MeshData> = Vec::new();
1160 let mut processed_jobs = 0usize;
1161 let mut total_meshes = 0usize;
1162 let mut total_vertices = 0usize;
1163 let mut total_triangles = 0usize;
1164 let mut chunk_start = 0usize;
1165 let mut current_chunk_size = initial_chunk_size;
1166
1167 let mut deferred_styles_applied = !defer_style_updates;
1168
1169 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1172
1173 let item_dedup_cache = GeometryRouter::new_dedup_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 EntityDecoder::with_arc_index(content, entity_index_for_meta.clone());
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 &mapped_item_cache,
1429 instancing_plan.as_ref(),
1430 indexed_colour_split_ids.as_ref(),
1431 &raw_instance_collector,
1432 worker_point_cache,
1433 worker_placement_cache,
1434 &point_cache_hits_collector,
1435 &point_cache_misses_collector,
1436 &faceted_brep_ns_collector,
1437 )
1438 })
1439 .flatten_iter()
1440 .collect();
1441
1442 processed_jobs += jobs_chunk.len();
1443 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1444 total_triangles += chunk_meshes
1445 .iter()
1446 .map(|m| m.triangle_count())
1447 .sum::<usize>();
1448
1449 if !chunk_meshes.is_empty() {
1450 total_meshes += chunk_meshes.len();
1451 let emit_mesh_chunk_size = current_chunk_size.max(1);
1452 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1453 on_batch(emitted_meshes, processed_jobs, total_jobs);
1454 }
1455 if options.retain_emitted_meshes {
1456 meshes.extend(chunk_meshes);
1457 }
1458
1459 if !deferred_styles_applied {
1460 let mut rebuilt_styles = {
1465 let mut style_decoder =
1466 EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1467 crate::prepass::resolve_styled_item_spans(
1468 &deferred_styled_item_positions,
1469 &mut style_decoder,
1470 )
1471 };
1472 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1473 geometry_style_index = Arc::new(rebuilt_styles);
1474 let deferred_color_updates = build_color_updates_for_jobs(
1475 &entity_jobs[..processed_jobs],
1476 geometry_style_index.as_ref(),
1477 content,
1478 &entity_index_arc,
1479 );
1480 if !deferred_color_updates.is_empty() {
1481 on_color_update(&deferred_color_updates);
1482 }
1483 deferred_styles_applied = true;
1484 }
1485 }
1486 chunk_start = chunk_end;
1487 current_chunk_size = throughput_chunk_size;
1488 }
1489
1490 let geometry_time = geometry_start.elapsed();
1491 let csg_failures = diag_collectors
1494 .csg_failures
1495 .into_inner()
1496 .unwrap_or_else(|poisoned| poisoned.into_inner());
1497 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1498 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1499 let backstop_dropped = diag_collectors.backstop.into_inner();
1500 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1502 let point_cache_hits = point_cache_hits_collector.into_inner();
1503 let point_cache_misses = point_cache_misses_collector.into_inner();
1504 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1505 geometry_span.record("mesh_count", total_meshes as u64);
1506 geometry_span.record("triangle_count", total_triangles as u64);
1507 geometry_span.record("backstop_count", backstop_dropped);
1508 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1509 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1510 drop(geometry_guard);
1511 if total_csg_failures > 0 {
1512 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1513 for fails in csg_failures.values() {
1514 for f in fails {
1515 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1516 }
1517 }
1518 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1519 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1520 let breakdown = breakdown
1521 .iter()
1522 .map(|(reason, count)| format!("{reason}={count}"))
1523 .collect::<Vec<_>>()
1524 .join(" ");
1525 tracing::warn!(
1526 total_csg_failures,
1527 products_with_failures,
1528 %breakdown,
1529 "CSG failures during geometry extraction (cut dropped, host kept uncut)"
1530 );
1531 }
1532
1533 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1534 diagnostics::collate(
1535 diag_collectors.classification,
1536 diag_collectors.host_diags,
1537 diag_collectors.rect_fast,
1538 diag_collectors.unsupported_items,
1539 &csg_failures,
1540 oversized_ref_drops,
1541 )
1542 });
1543
1544 let instances = instancing::finalize_instances(
1549 raw_instance_collector
1550 .into_inner()
1551 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1552 &mut meshes,
1553 &mapped_item_cache,
1554 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1555 );
1556
1557 let total_time = total_start.elapsed();
1558 pipeline_span.record("element_count", total_jobs as u64);
1559 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1560
1561 tracing::info!(
1562 meshes = meshes.len(),
1563 instances = instances.len(),
1564 vertices = total_vertices,
1565 triangles = total_triangles,
1566 backstop_count = backstop_dropped,
1567 geometry_time_ms = geometry_time.as_millis(),
1568 total_time_ms = total_time.as_millis(),
1569 "Geometry processing complete"
1570 );
1571
1572 ProcessingResult {
1573 meshes,
1574 instances,
1575 mesh_coordinate_space: Some(coord_space.to_string()),
1576 site_transform,
1577 building_transform,
1578 metadata: ModelMetadata {
1579 schema_version,
1580 entity_count: total_entities,
1581 geometry_entity_count,
1582 coordinate_info: CoordinateInfo {
1583 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1584 is_geo_referenced: has_rtc_offset,
1585 },
1586 length_unit_scale: Some(unit_scale),
1587 georeferencing: crate::extract_georeferencing_with_index(content, &entity_index_arc),
1588 },
1589 stats: ProcessingStats {
1590 total_meshes,
1591 total_vertices,
1592 total_triangles,
1593 parse_time_ms: parse_time.as_millis() as u64,
1594 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1595 lookup_time_ms: lookup_time.as_millis() as u64,
1596 preprocess_time_ms: preprocess_time.as_millis() as u64,
1597 geometry_time_ms: geometry_time.as_millis() as u64,
1598 total_time_ms: total_time.as_millis() as u64,
1599 from_cache: false,
1600 total_csg_failures: total_csg_failures as u64,
1601 products_with_failures,
1602 degenerate_triangles_dropped: backstop_dropped,
1603 point_cache_hits,
1604 point_cache_misses,
1605 faceted_brep_time_ms,
1606 geometry_diagnostics,
1607 },
1608 }
1609}
1610
1611