1use crate::types::mesh::{InstanceRecord, MeshData, RawInstanceOccurrence};
10use crate::types::response::{
11 CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
12 QuickMetadataEntitySummary,
13};
14use ifc_lite_core::{
15 keyword_eq, keyword_starts_with, 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 csg_summary;
27#[cfg(test)]
28#[path = "csg_summary_tests.rs"]
29mod csg_summary_tests;
30mod diagnostics;
31mod entity_index;
32use entity_index::{IndexBuilder, ProcessingIndex};
33pub(crate) mod instancing;
34mod jobs;
35mod opening_filter;
36mod properties;
37mod quick_metadata;
38mod schema_detection;
39mod site_local;
40
41pub use quick_metadata::is_quick_spatial_type_ci;
42pub use site_local::{convert_mesh_to_site_local, native_to_baked};
43pub(crate) use site_local::site_local_rotation_invalidates_captured_transforms;
44
45use jobs::{build_color_updates_for_jobs, process_entity_job};
46
47pub(crate) use color_layer::resolve_element_color_for_product_definition_shape;
48use color_layer::{
49 collect_presentation_layer_assignments,
50 resolve_presentation_layer_for_product_definition_shape,
51};
52use opening_filter::apply_opening_filter;
53use properties::resolve_space_zone_properties_lazy;
54use quick_metadata::{
55 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
56 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
57};
58use crate::mesh_frame::{MeshCoordinateSpace, MeshFrame};
59
60#[cfg(not(target_arch = "wasm32"))]
67use std::time::Instant as Clock;
68
69#[cfg(target_arch = "wasm32")]
70#[derive(Clone, Copy)]
71struct Clock;
72
73#[cfg(target_arch = "wasm32")]
74impl Clock {
75 #[inline]
76 fn now() -> Self {
77 Clock
78 }
79 #[inline]
80 fn elapsed(&self) -> std::time::Duration {
81 std::time::Duration::ZERO
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum OpeningFilterMode {
89 #[default]
91 Default = 0,
92 IgnoreAll = 1,
94 IgnoreOpaque = 2,
96}
97
98impl OpeningFilterMode {
99 pub fn cache_key_suffix(&self) -> &'static str {
102 match self {
103 Self::Default => "default",
104 Self::IgnoreAll => "ignore_all",
105 Self::IgnoreOpaque => "ignore_opaque",
106 }
107 }
108}
109
110pub struct ProcessingResult {
112 pub meshes: Vec<MeshData>,
113 pub instances: Vec<InstanceRecord>,
120 pub mesh_coordinate_space: MeshCoordinateSpace,
126 pub frame: MeshFrame,
135 pub site_transform: Option<Vec<f64>>,
137 pub building_transform: Option<Vec<f64>>,
139 pub metadata: ModelMetadata,
140 pub stats: ProcessingStats,
141}
142
143#[derive(Debug, Clone)]
147pub struct StreamingOptions {
148 pub initial_batch_size: usize,
150 pub throughput_batch_size: usize,
152 pub fast_first_batch: bool,
154 pub include_properties: bool,
156 pub include_presentation_layers: bool,
158 pub emit_quick_metadata_bootstrap: bool,
160 pub retain_emitted_meshes: bool,
162 pub tessellation_quality: TessellationQuality,
169 pub entity_index: Option<Arc<EntityIndex>>,
177 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
183 pub enable_instancing: bool,
192}
193
194impl Default for StreamingOptions {
195 fn default() -> Self {
196 Self {
197 initial_batch_size: 50,
198 throughput_batch_size: 50,
199 fast_first_batch: false,
200 include_properties: true,
201 include_presentation_layers: true,
202 emit_quick_metadata_bootstrap: false,
203 retain_emitted_meshes: true,
204 tessellation_quality: TessellationQuality::default(),
205 entity_index: None,
206 cancel: None,
207 enable_instancing: false,
208 }
209 }
210}
211
212pub(super) struct EntityJob {
214 pub(super) id: u32,
215 pub(super) ifc_type: IfcType,
216 pub(super) start: usize,
217 pub(super) end: usize,
218 pub(super) product_definition_shape_id: Option<u32>,
219 pub(super) element_color: [f32; 4],
220 pub(super) global_id: Option<String>,
221 pub(super) name: Option<String>,
222 pub(super) presentation_layer: Option<String>,
223 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
224 pub(super) representation_map_id: Option<u32>,
228}
229
230#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
232#[allow(clippy::too_many_arguments)]
234fn populate_entity_job_metadata(
235 job: &mut EntityJob,
236 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
237 element_material_color: &FxHashMap<u32, [f32; 4]>,
238 layer_by_assigned_representation: &FxHashMap<u32, String>,
239 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
240 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
241 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
242 decoder: &mut EntityDecoder,
243 include_presentation_layers: bool,
244) {
245 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
246 return;
247 }
248
249 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
250 return;
251 };
252
253 job.global_id = normalize_optional_string(entity.get_string(0));
254 job.name = normalize_optional_string(entity.get_string(2));
255 job.product_definition_shape_id = entity.get_ref(6);
256
257 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
258 return;
259 };
260
261 let resolved_color = color_cache_by_product_definition_shape
262 .entry(product_definition_shape_id)
263 .or_insert_with(|| {
264 resolve_element_color_for_product_definition_shape(
265 product_definition_shape_id,
266 geometry_style_index,
267 decoder,
268 )
269 });
270 if let Some(color) = resolved_color {
271 job.element_color = *color;
272 } else if let Some(color) = element_material_color.get(&job.id) {
273 job.element_color = *color;
274 }
275
276 if include_presentation_layers {
277 let resolved_layer = layer_cache_by_product_definition_shape
278 .entry(product_definition_shape_id)
279 .or_insert_with(|| {
280 resolve_presentation_layer_for_product_definition_shape(
281 product_definition_shape_id,
282 layer_by_assigned_representation,
283 layer_cache_by_representation,
284 decoder,
285 )
286 });
287 job.presentation_layer = resolved_layer.clone();
288 }
289}
290
291use crate::style::GeometryStyleInfo;
295
296pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
297 let value = raw?.trim();
298 if value.is_empty() || value == "$" {
299 return None;
300 }
301 Some(value.to_string())
302}
303
304fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
305 match ifc_type {
306 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
307 IfcType::IfcSlab => 95,
308 IfcType::IfcColumn => 90,
309 IfcType::IfcBeam => 85,
310 IfcType::IfcRoof => 80,
311 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
312 IfcType::IfcCurtainWall => 70,
313 IfcType::IfcFooting | IfcType::IfcPile => 65,
314 IfcType::IfcDoor | IfcType::IfcWindow => 30,
315 IfcType::IfcFurnishingElement => 10,
316 _ => 50,
317 }
318}
319
320pub fn process_geometry<T>(content: &T) -> ProcessingResult
322where
323 T: AsRef<[u8]> + ?Sized,
324{
325 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
326}
327
328pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
335where
336 T: AsRef<[u8]> + ?Sized,
337{
338 process_geometry_streaming_filtered_with_options(
339 content.as_ref(),
340 OpeningFilterMode::Default,
341 StreamingOptions {
342 initial_batch_size: usize::MAX,
343 throughput_batch_size: usize::MAX,
344 entity_index: Some(index),
345 ..StreamingOptions::default()
346 },
347 |_, _, _| {},
348 |_| {},
349 |_| {},
350 )
351}
352
353pub fn process_geometry_streaming(
355 content: &[u8],
356 batch_size: usize,
357 on_batch: impl FnMut(&[MeshData], usize, usize),
358) -> ProcessingResult {
359 process_geometry_streaming_with_options(
360 content,
361 StreamingOptions {
362 initial_batch_size: batch_size,
363 throughput_batch_size: batch_size,
364 ..StreamingOptions::default()
365 },
366 on_batch,
367 |_| {},
368 )
369}
370
371pub fn process_geometry_streaming_with_options(
373 content: &[u8],
374 options: StreamingOptions,
375 on_batch: impl FnMut(&[MeshData], usize, usize),
376 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
377) -> ProcessingResult {
378 process_geometry_streaming_with_options_and_bootstrap(
379 content,
380 options,
381 on_batch,
382 on_color_update,
383 |_| {},
384 )
385}
386
387pub fn process_geometry_streaming_with_options_and_bootstrap(
390 content: &[u8],
391 options: StreamingOptions,
392 on_batch: impl FnMut(&[MeshData], usize, usize),
393 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
394 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
395) -> ProcessingResult {
396 process_geometry_streaming_filtered_with_options(
397 content,
398 OpeningFilterMode::Default,
399 options,
400 on_batch,
401 on_color_update,
402 on_quick_metadata_bootstrap,
403 )
404}
405
406pub fn process_geometry_filtered<T>(
408 content: &T,
409 opening_filter: OpeningFilterMode,
410) -> ProcessingResult
411where
412 T: AsRef<[u8]> + ?Sized,
413{
414 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
415}
416
417pub fn process_geometry_filtered_with_quality<T>(
421 content: &T,
422 opening_filter: OpeningFilterMode,
423 tessellation_quality: TessellationQuality,
424) -> ProcessingResult
425where
426 T: AsRef<[u8]> + ?Sized,
427{
428 let content = content.as_ref();
429 process_geometry_streaming_filtered_with_options(
430 content,
431 opening_filter,
432 StreamingOptions {
433 initial_batch_size: usize::MAX,
434 throughput_batch_size: usize::MAX,
435 tessellation_quality,
436 ..StreamingOptions::default()
437 },
438 |_, _, _| {},
439 |_| {},
440 |_| {},
441 )
442}
443
444pub fn process_geometry_streaming_filtered(
446 content: &[u8],
447 opening_filter: OpeningFilterMode,
448 batch_size: usize,
449 on_batch: impl FnMut(&[MeshData], usize, usize),
450 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
451) -> ProcessingResult {
452 process_geometry_streaming_filtered_with_options(
453 content,
454 opening_filter,
455 StreamingOptions {
456 initial_batch_size: batch_size,
457 throughput_batch_size: batch_size,
458 ..StreamingOptions::default()
459 },
460 on_batch,
461 on_color_update,
462 |_| {},
463 )
464}
465
466pub fn process_geometry_streaming_filtered_with_options(
468 content: &[u8],
469 opening_filter: OpeningFilterMode,
470 options: StreamingOptions,
471 mut on_batch: impl FnMut(&[MeshData], usize, usize),
472 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
473 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
474) -> ProcessingResult {
475 let total_start = Clock::now();
476 let parse_start = Clock::now();
477 let entity_scan_start = Clock::now();
478
479 let pipeline_span = tracing::info_span!(
486 "geometry_pipeline",
487 byte_size = content.len(),
488 element_count = tracing::field::Empty,
489 total_ms = tracing::field::Empty,
490 );
491 let _pipeline_guard = pipeline_span.clone().entered();
492
493 tracing::info!(
494 content_size = content.len(),
495 "Starting IFC geometry processing"
496 );
497
498 let scan_span = tracing::info_span!(
499 "scan_prepass",
500 total_entities = tracing::field::Empty,
501 geometry_entities = tracing::field::Empty,
502 phase_ms = tracing::field::Empty,
503 );
504 let scan_guard = scan_span.clone().entered();
505
506 let provided_index = options.entity_index.clone();
513 let building_index = provided_index.is_none();
514 let mut inline_index = IndexBuilder::new(content.len(), building_index);
515 let mut decoder = match &provided_index {
516 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
517 None => EntityDecoder::new(content),
518 };
519 tracing::debug!("Entity index will be built inline during the scan");
520
521 let mut prepass_spans = crate::prepass::PrepassSpans::default();
527 let mut project_id: Option<u32> = None;
528 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
529 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
536
537 let mut scanner = EntityScanner::new(content);
539 let mut georeferencing_candidates = Vec::new();
540 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
541 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
546 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
547 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
552 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
561 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
562 let mut quick_spatial_nodes =
563 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
564 let mut quick_aggregate_links = if quick_metadata_enabled {
565 Vec::<(u32, Vec<u32>)>::new()
566 } else {
567 Vec::new()
568 };
569 let mut quick_containment_links = if quick_metadata_enabled {
570 Vec::<(u32, Vec<u32>)>::new()
571 } else {
572 Vec::new()
573 };
574 let mut quick_referenced_links = if quick_metadata_enabled {
579 Vec::<(u32, Vec<u32>)>::new()
580 } else {
581 Vec::new()
582 };
583 let mut quick_element_summaries = if quick_metadata_enabled {
584 HashMap::<u32, QuickMetadataEntitySummary>::new()
585 } else {
586 HashMap::new()
587 };
588 let mut total_entities = 0usize;
589 let mut site_entity_pos: Option<(usize, usize)> = None;
590 let mut building_entity_pos: Option<(usize, usize)> = None;
591
592 let defer_style_updates = options.fast_first_batch
593 && opening_filter == OpeningFilterMode::Default
594 && !options.include_presentation_layers;
595
596 while let Some((id, type_name, start, end)) = scanner.next_entity() {
597 total_entities += 1;
598 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
599 georeferencing_candidates.push((id, ifc_type));
600 }
601 if building_index {
602 inline_index.insert(id, (start, end));
603 }
604 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
605 if is_quick_spatial_type_ci(type_name) {
607 let args = parse_step_arguments(&content[start..end]);
608 let fallback = format!("{type_name} #{id}");
609 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
610 express_id: id,
611 type_name: type_name.to_string(),
612 name: extract_name_from_args(&args, &fallback),
613 elevation: if keyword_eq(type_name, "IFCBUILDINGSTOREY") {
614 extract_storey_elevation_from_args(&args)
615 } else {
616 None
617 },
618 children: Vec::new(),
619 contained: Vec::new(),
620 elements: Vec::new(),
621 named_as_child: false,
622 });
623 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
624 let args = parse_step_arguments(&content[start..end]);
625 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
626 quick_aggregate_links.push((
627 parent_id,
628 args.get(5)
629 .map(|token| parse_step_ref_list(token))
630 .unwrap_or_default(),
631 ));
632 }
633 } else if keyword_eq(type_name, "IFCRELCONTAINEDINSPATIALSTRUCTURE") {
634 let args = parse_step_arguments(&content[start..end]);
635 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
636 quick_containment_links.push((
637 parent_id,
638 args.get(4)
639 .map(|token| parse_step_ref_list(token))
640 .unwrap_or_default(),
641 ));
642 }
643 } else if keyword_eq(type_name, "IFCRELREFERENCEDINSPATIALSTRUCTURE") {
644 let args = parse_step_arguments(&content[start..end]);
645 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
646 quick_referenced_links.push((
647 parent_id,
648 args.get(4)
649 .map(|token| parse_step_ref_list(token))
650 .unwrap_or_default(),
651 ));
652 }
653 }
654 }
655
656 if keyword_eq(type_name, "IFCINDEXEDCOLOURMAP") {
657 prepass_spans.indexed_colour_maps.push((id, start, end));
659 continue;
660 }
661
662 if keyword_eq(type_name, "IFCSTYLEDITEM") {
663 prepass_spans.styled_items.push((id, start, end));
667 continue;
668 } else if keyword_eq(type_name, "IFCMATERIALDEFINITIONREPRESENTATION") {
669 prepass_spans.material_def_reprs.push((id, start, end));
670 continue;
671 } else if keyword_eq(type_name, "IFCRELASSOCIATESMATERIAL") {
672 prepass_spans.rel_associates_material.push((id, start, end));
673 continue;
674 } else if keyword_eq(type_name, "IFCPRESENTATIONLAYERASSIGNMENT") {
675 if !options.include_presentation_layers {
676 continue;
677 }
678 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
679 collect_presentation_layer_assignments(
680 &mut presentation_layer_by_assigned_id,
681 &layer_assignment,
682 );
683 }
684 continue;
685 } else if keyword_eq(type_name, "IFCPROPERTYSET") {
686 continue;
689 } else if keyword_eq(type_name, "IFCRELDEFINESBYPROPERTIES") {
690 if options.include_properties {
691 rel_defines_spans.push((start, end));
692 }
693 continue;
694 } else if keyword_starts_with(type_name, "IFCPROPERTY") {
695 continue;
698 } else if keyword_eq(type_name, "IFCRELVOIDSELEMENT") {
699 prepass_spans.void_rels.push((id, start, end));
700 } else if keyword_eq(type_name, "IFCRELFILLSELEMENT") {
701 prepass_spans.fills_rels.push((id, start, end));
702 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
703 prepass_spans.aggregate_rels.push((id, start, end));
708 } else if keyword_eq(type_name, "IFCPROJECT") && project_id.is_none() {
709 project_id = Some(id);
710 } else if keyword_eq(type_name, "IFCSITE") && site_entity_pos.is_none() {
711 site_entity_pos = Some((start, end));
712 } else if keyword_eq(type_name, "IFCBUILDING") && building_entity_pos.is_none() {
713 building_entity_pos = Some((start, end));
714 }
715
716 let (has_geometry, representationless_spatial) =
717 ifc_lite_core::geometry_flags_by_name(type_name);
718 if has_geometry {
719 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
723 if quick_metadata_enabled {
724 quick_element_summaries.insert(
725 id,
726 QuickMetadataEntitySummary {
727 express_id: id,
728 type_name: type_name.to_string(),
729 name: format!("{type_name} #{id}"),
730 global_id: None,
731 kind: "element".to_string(),
732 has_children: false,
733 element_count: None,
734 elevation: None,
735 },
736 );
737 }
738 entity_jobs.push(EntityJob {
739 id,
740 ifc_type,
741 start,
742 end,
743 product_definition_shape_id: None,
744 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
745 global_id: None,
746 name: None,
747 presentation_layer: None,
748 space_zone_properties: None,
749 representation_map_id: None,
750 });
751 } else if representationless_spatial
752 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
753 {
754 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
766 entity_jobs.push(EntityJob {
767 id,
768 ifc_type,
769 start,
770 end,
771 product_definition_shape_id: None,
772 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
773 global_id: None,
774 name: None,
775 presentation_layer: None,
776 space_zone_properties: None,
777 representation_map_id: None,
778 });
779 }
780 else if keyword_eq(type_name, "IFCMAPPEDITEM") {
787 let args = parse_step_arguments(&content[start..end]);
788 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
789 referenced_representation_maps.insert(source_id);
790 if options.enable_instancing {
794 mapped_item_plan
795 .entry(source_id)
796 .and_modify(|(count, template)| {
797 *count += 1;
798 if id < *template {
799 *template = id;
800 }
801 })
802 .or_insert((1, id));
803 }
804 }
805 } else if keyword_eq(type_name, "IFCRELDEFINESBYTYPE") {
806 let args = parse_step_arguments(&content[start..end]);
809 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
810 instantiated_type_ids.insert(type_id);
811 }
812 prepass_spans.defines_by_type.push((id, start, end));
814 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
815 let args = parse_step_arguments(&content[start..end]);
816 let rep_map_ids = args
818 .get(6)
819 .map(|token| parse_step_ref_list(token))
820 .unwrap_or_default();
821 if !rep_map_ids.is_empty() {
822 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
823 }
824 }
825 }
826
827 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
829
830 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
838 for (rep_map_id, _class) in crate::element::plan_type_geometry(
843 rep_map_ids,
844 &referenced_representation_maps,
845 instantiated_type_ids.contains(type_id),
846 crate::element::TypeGeometryMode::SuppressInstanced,
847 ) {
848 entity_jobs.push(EntityJob {
849 id: *type_id,
850 ifc_type: *ifc_type,
851 start: *start,
852 end: *end,
853 product_definition_shape_id: None,
854 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
855 global_id: None,
856 name: None,
857 presentation_layer: None,
858 space_zone_properties: None,
859 representation_map_id: Some(rep_map_id),
860 });
861 }
862 }
863
864 let entity_index = match provided_index {
869 Some(idx) => ProcessingIndex::Hash(idx),
870 None => inline_index.finish(),
871 };
872 entity_index.install(&mut decoder);
873
874 let resolved = crate::prepass::resolve_prepass(
879 &prepass_spans,
880 &mut decoder,
881 crate::prepass::ResolveOptions {
882 collect_indexed_colour_full: true,
883 defer_attached_styles: defer_style_updates,
884 },
885 );
886 let crate::prepass::ResolvedPrepass {
887 mut geometry_style_index,
888 indexed_colour_index,
889 indexed_colour_full,
890 element_material_colors,
891 void_index,
892 filling_by_opening,
893 deferred_attached_styled_spans: deferred_styled_item_positions,
894 ..
895 } = resolved;
896
897 let entity_scan_time = entity_scan_start.elapsed();
898 scan_span.record("total_entities", total_entities as u64);
899 scan_span.record("geometry_entities", entity_jobs.len() as u64);
900 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
901 drop(scan_guard);
902
903 let lookup_start = Clock::now();
904 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
905 if options.include_properties {
906 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
907 }
908 if options.fast_first_batch {
909 entity_jobs.sort_by(|left, right| {
910 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
911 });
912 }
913 let lookup_time = lookup_start.elapsed();
914 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
915 drop(lookup_span);
916
917 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
921 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
922 &entity_jobs,
923 &void_index,
924 &filling_by_opening,
925 &geometry_style_index,
926 &element_material_colors,
927 &mut decoder,
928 opening_filter,
929 );
930
931 let schema_version = schema_detection::detect_schema_version(content).to_string();
932
933 let geometry_entity_count = entity_jobs.len();
934 tracing::info!(
935 total_entities = total_entities,
936 geometry_entities = geometry_entity_count,
937 voids = void_index.len(),
938 schema_version = %schema_version,
939 "Entity scanning complete"
940 );
941
942 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
943 for (parent_id, child_ids) in quick_aggregate_links {
944 if !spatial_nodes.contains_key(&parent_id) {
945 continue;
946 }
947 for child_id in child_ids {
948 if !spatial_nodes.contains_key(&child_id) {
949 continue;
950 }
951 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
952 parent.children.push(child_id);
953 }
954 if let Some(child) = spatial_nodes.get_mut(&child_id) {
955 child.named_as_child = true;
956 }
957 }
958 }
959 for (parent_id, element_ids) in quick_containment_links {
960 if !spatial_nodes.contains_key(&parent_id) {
961 continue;
962 }
963 for child_id in element_ids {
964 if spatial_nodes.contains_key(&child_id) {
977 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
978 parent.contained.push(child_id);
979 }
980 if let Some(child) = spatial_nodes.get_mut(&child_id) {
981 child.named_as_child = true;
982 }
983 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
984 parent.elements.push(child_id);
985 }
986 }
987 }
988 for (parent_id, element_ids) in quick_referenced_links {
992 if !spatial_nodes.contains_key(&parent_id) {
993 continue;
994 }
995 for child_id in element_ids {
996 if spatial_nodes.contains_key(&child_id) {
999 continue;
1000 }
1001 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1002 parent.elements.push(child_id);
1003 }
1004 }
1005 }
1006 let root_id = spatial_nodes
1010 .values()
1011 .filter(|node| keyword_eq(&node.type_name, "IFCPROJECT"))
1012 .map(|node| node.express_id)
1013 .min()
1014 .or_else(|| {
1015 spatial_nodes
1019 .values()
1020 .min_by_key(|node| (node.named_as_child, node.express_id))
1021 .map(|node| node.express_id)
1022 });
1023 let (spatial_tree, pruned_aggregate_edges) = root_id
1024 .and_then(|root| {
1025 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries).ok()
1026 })
1027 .unzip();
1028 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1029 schema_version: schema_version.clone(),
1030 entity_count: total_entities,
1031 spatial_tree,
1032 pruned_aggregate_edges: pruned_aggregate_edges.unwrap_or_default(),
1033 });
1034 }
1035
1036 let preprocess_start = Clock::now();
1038 let preprocess_span =
1039 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1040 let unit_scales = tracing::debug_span!("unit_scale")
1046 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1047 tracing::debug!(
1048 length_unit_scale = unit_scales.length_unit_scale,
1049 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1050 "Resolved unit scales"
1051 );
1052 decoder.seed_unit_scales(
1053 unit_scales.length_unit_scale,
1054 unit_scales.plane_angle_to_radians,
1055 );
1056 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1058 router.set_tessellation_quality(options.tessellation_quality);
1059 let material_spans = &prepass_spans.rel_associates_material;
1063 router.set_material_layer_index(Arc::new(
1064 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1065 ));
1066
1067 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1069 let entity = decoder.decode_at(start, end).ok()?;
1070 let matrix = router
1071 .resolve_scaled_placement(&entity, &mut decoder)
1072 .ok()?;
1073 Some(matrix.to_vec())
1074 });
1075 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1076 let entity = decoder.decode_at(start, end).ok()?;
1077 let matrix = router
1078 .resolve_scaled_placement(&entity, &mut decoder)
1079 .ok()?;
1080 Some(matrix.to_vec())
1081 });
1082
1083 let detected_rtc_offset = router.detect_rtc_offset_for_file(content, &mut decoder);
1088
1089 let frame = MeshFrame::select(site_transform.as_deref(), detected_rtc_offset);
1092 let coord_space = frame.coordinate_space();
1093 let has_rtc_offset = frame.needs_shift();
1094 router.set_rtc_offset(frame.rtc_offset());
1095 let preprocess_time = preprocess_start.elapsed();
1096 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1097 drop(preprocess_span);
1098
1099 let parse_time = parse_start.elapsed();
1100 tracing::info!(
1101 entity_scan_time_ms = entity_scan_time.as_millis(),
1102 lookup_time_ms = lookup_time.as_millis(),
1103 preprocess_time_ms = preprocess_time.as_millis(),
1104 parse_time_ms = parse_time.as_millis(),
1105 "Parse phase complete, starting geometry extraction"
1106 );
1107
1108 let geometry_start = Clock::now();
1110 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1112 let rtc_offset = router.rtc_offset();
1113 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1121 let void_index_arc = Arc::new(filtered_void_index);
1122 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1123 let mut geometry_style_index = Arc::new(geometry_style_index);
1124 let indexed_colour_full = Arc::new(indexed_colour_full);
1125 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1130 content,
1131 &mut decoder,
1132 ));
1133 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1137 .iter()
1138 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1139 .collect();
1140 let element_material_colors = Arc::new(element_material_colors);
1141
1142 let total_jobs = entity_jobs.len();
1143 let initial_chunk_size = options.initial_batch_size.max(1);
1144 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1145 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1146 FxHashMap::default();
1147 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1148 FxHashMap::default();
1149 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1150 let mut meshes: Vec<MeshData> = Vec::new();
1151 let mut processed_jobs = 0usize;
1152 let mut total_meshes = 0usize;
1153 let mut total_vertices = 0usize;
1154 let mut total_triangles = 0usize;
1155 let mut chunk_start = 0usize;
1156 let mut current_chunk_size = initial_chunk_size;
1157
1158 let mut deferred_styles_applied = !defer_style_updates;
1159
1160 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1163
1164 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1169 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1170
1171 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1175
1176 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1206 && options.retain_emitted_meshes
1207 && coord_space != MeshCoordinateSpace::SiteLocal)
1208 .then(|| {
1209 Arc::new(
1210 mapped_item_plan
1211 .into_iter()
1212 .filter(|(_, (count, _))| *count >= 2)
1213 .collect::<FxHashMap<u32, (u32, u32)>>(),
1214 )
1215 });
1216 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1235 && !indexed_colour_full.is_empty())
1236 .then(|| {
1237 let ids: FxHashSet<u32> = indexed_colour_full
1238 .iter()
1239 .filter(|(_, m)| m.has_multiple_colours())
1240 .map(|(&id, _)| id)
1241 .collect();
1242 (!ids.is_empty()).then(|| Arc::new(ids))
1243 })
1244 .flatten();
1245 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1248 std::sync::Mutex::new(Vec::new());
1249
1250 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1258 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1259 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1260
1261 let worker_point_caches = jobs::new_worker_point_caches();
1262 let worker_placement_caches = jobs::new_worker_placement_caches();
1266
1267 let geometry_span = tracing::info_span!(
1268 "geometry",
1269 element_count = total_jobs,
1270 mesh_count = tracing::field::Empty,
1271 triangle_count = tracing::field::Empty,
1272 backstop_count = tracing::field::Empty,
1273 total_csg_failures = tracing::field::Empty,
1274 phase_ms = tracing::field::Empty,
1275 );
1276 let geometry_guard = geometry_span.clone().entered();
1277
1278 while chunk_start < total_jobs {
1279 if options
1283 .cancel
1284 .as_ref()
1285 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1286 {
1287 break;
1288 }
1289 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1290 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1291
1292 #[cfg(not(target_arch = "wasm32"))]
1296 {
1297 let entity_index_for_meta = entity_index_arc.clone();
1299 jobs_chunk.par_iter_mut().for_each(|job| {
1300 if job.global_id.is_some()
1301 || job.name.is_some()
1302 || job.product_definition_shape_id.is_some()
1303 {
1304 return;
1305 }
1306 let local_decoder = entity_index_for_meta.decoder(content);
1307 let Ok(entity) = local_decoder.decode_at_uncached(job.start, job.end) else {
1309 return;
1310 };
1311 job.global_id = normalize_optional_string(entity.get_string(0));
1312 job.name = normalize_optional_string(entity.get_string(2));
1313 job.product_definition_shape_id = entity.get_ref(6);
1314 });
1315
1316 for job in jobs_chunk.iter_mut() {
1318 let Some(pds_id) = job.product_definition_shape_id else {
1319 continue;
1320 };
1321 let resolved_color = color_cache_by_product_definition_shape
1322 .entry(pds_id)
1323 .or_insert_with(|| {
1324 resolve_element_color_for_product_definition_shape(
1325 pds_id,
1326 &geometry_style_index,
1327 &mut decoder,
1328 )
1329 });
1330 if let Some(color) = resolved_color {
1331 job.element_color = *color;
1332 } else if let Some(color) = element_material_color.get(&job.id) {
1333 job.element_color = *color;
1336 }
1337 if options.include_presentation_layers {
1338 let resolved_layer = layer_cache_by_product_definition_shape
1339 .entry(pds_id)
1340 .or_insert_with(|| {
1341 resolve_presentation_layer_for_product_definition_shape(
1342 pds_id,
1343 &presentation_layer_by_assigned_id,
1344 &mut layer_cache_by_representation,
1345 &mut decoder,
1346 )
1347 });
1348 job.presentation_layer = resolved_layer.clone();
1349 }
1350 }
1351 }
1352
1353 #[cfg(target_arch = "wasm32")]
1355 for job in jobs_chunk.iter_mut() {
1356 populate_entity_job_metadata(
1357 job,
1358 &geometry_style_index,
1359 &element_material_color,
1360 &presentation_layer_by_assigned_id,
1361 &mut color_cache_by_product_definition_shape,
1362 &mut layer_cache_by_product_definition_shape,
1363 &mut layer_cache_by_representation,
1364 &mut decoder,
1365 options.include_presentation_layers,
1366 );
1367 }
1368 let site_local_rotation: Option<&Vec<f64>> =
1369 if coord_space == MeshCoordinateSpace::SiteLocal {
1370 site_transform.as_ref()
1371 } else {
1372 None
1373 };
1374 let chunk_meshes: Vec<MeshData> = jobs_chunk
1379 .par_iter()
1380 .map(|job| {
1381 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1382 let mut fallback_cache = FxHashMap::default();
1392 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1393 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1394 match slot_guard.as_deref_mut() {
1395 Some(cache) => cache,
1396 None => &mut fallback_cache,
1397 };
1398 let mut fallback_placement_cache = FxHashMap::default();
1407 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1408 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1409 match placement_slot_guard.as_deref_mut() {
1410 Some(cache) => cache,
1411 None => &mut fallback_placement_cache,
1412 };
1413 process_entity_job(
1414 job,
1415 content,
1416 &entity_index_arc,
1417 unit_scale,
1418 rtc_offset,
1419 seed_plane_angle_to_radians,
1420 options.tessellation_quality,
1421 void_index_arc.as_ref(),
1422 skipped_entity_ids.as_ref(),
1423 geometry_style_index.as_ref(),
1424 indexed_colour_full.as_ref(),
1425 element_material_colors.as_ref(),
1426 texture_index.as_ref(),
1427 site_local_rotation,
1428 &diag_collectors,
1429 &item_dedup_cache,
1430 &brep_signature_cache,
1431 &mapped_item_cache,
1432 instancing_plan.as_ref(),
1433 indexed_colour_split_ids.as_ref(),
1434 &raw_instance_collector,
1435 worker_point_cache,
1436 worker_placement_cache,
1437 &point_cache_hits_collector,
1438 &point_cache_misses_collector,
1439 &faceted_brep_ns_collector,
1440 )
1441 })
1442 .flatten_iter()
1443 .collect();
1444
1445 processed_jobs += jobs_chunk.len();
1446 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1447 total_triangles += chunk_meshes
1448 .iter()
1449 .map(|m| m.triangle_count())
1450 .sum::<usize>();
1451
1452 if !chunk_meshes.is_empty() {
1453 total_meshes += chunk_meshes.len();
1454 let emit_mesh_chunk_size = current_chunk_size.max(1);
1455 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1456 on_batch(emitted_meshes, processed_jobs, total_jobs);
1457 }
1458 if options.retain_emitted_meshes {
1459 meshes.extend(chunk_meshes);
1460 }
1461
1462 if !deferred_styles_applied {
1463 let mut rebuilt_styles = {
1468 let mut style_decoder =
1469 entity_index_arc.decoder(content);
1470 crate::prepass::resolve_styled_item_spans(
1471 &deferred_styled_item_positions,
1472 &mut style_decoder,
1473 )
1474 };
1475 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1476 geometry_style_index = Arc::new(rebuilt_styles);
1477 let deferred_color_updates = build_color_updates_for_jobs(
1478 &entity_jobs[..processed_jobs],
1479 geometry_style_index.as_ref(),
1480 content,
1481 &entity_index_arc,
1482 );
1483 if !deferred_color_updates.is_empty() {
1484 on_color_update(&deferred_color_updates);
1485 }
1486 deferred_styles_applied = true;
1487 }
1488 }
1489 chunk_start = chunk_end;
1490 current_chunk_size = throughput_chunk_size;
1491 }
1492
1493 let geometry_time = geometry_start.elapsed();
1494 let csg_failures = diag_collectors
1497 .csg_failures
1498 .into_inner()
1499 .unwrap_or_else(|poisoned| poisoned.into_inner());
1500 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1501 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1502 let backstop_dropped = diag_collectors.backstop.into_inner();
1503 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1505 let point_cache_hits = point_cache_hits_collector.into_inner();
1506 let point_cache_misses = point_cache_misses_collector.into_inner();
1507 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1508 geometry_span.record("mesh_count", total_meshes as u64);
1509 geometry_span.record("triangle_count", total_triangles as u64);
1510 geometry_span.record("backstop_count", backstop_dropped);
1511 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1512 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1513 drop(geometry_guard);
1514 if total_csg_failures > 0 {
1515 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1516 for fails in csg_failures.values() {
1517 for f in fails {
1518 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1519 }
1520 }
1521 let open_topology_accepted = *by_reason.get("KernelError").unwrap_or(&0);
1526 let dropped = total_csg_failures - open_topology_accepted;
1527 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1528 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1529 let breakdown = breakdown
1530 .iter()
1531 .map(|(reason, count)| format!("{reason}={count}"))
1532 .collect::<Vec<_>>()
1533 .join(" ");
1534 tracing::warn!(
1535 total_csg_failures,
1536 products_with_failures,
1537 dropped,
1538 open_topology_accepted,
1539 %breakdown,
1540 "{}",
1541 csg_summary::csg_summary_message(dropped, open_topology_accepted)
1542 );
1543 }
1544
1545 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1546 diagnostics::collate(
1547 diag_collectors.classification,
1548 diag_collectors.host_diags,
1549 diag_collectors.rect_fast,
1550 diag_collectors.unsupported_items,
1551 &csg_failures,
1552 oversized_ref_drops,
1553 )
1554 });
1555
1556 let instances = instancing::finalize_instances(
1561 raw_instance_collector
1562 .into_inner()
1563 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1564 &mut meshes,
1565 &mapped_item_cache,
1566 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1567 );
1568
1569 let total_time = total_start.elapsed();
1570 pipeline_span.record("element_count", total_jobs as u64);
1571 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1572
1573 tracing::info!(
1574 meshes = meshes.len(),
1575 instances = instances.len(),
1576 vertices = total_vertices,
1577 triangles = total_triangles,
1578 backstop_count = backstop_dropped,
1579 geometry_time_ms = geometry_time.as_millis(),
1580 total_time_ms = total_time.as_millis(),
1581 "Geometry processing complete"
1582 );
1583
1584 let extract_georeferencing = || crate::georeferencing::extract_georeferencing_from_candidates(
1585 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1586 );
1587 #[cfg(not(target_arch = "wasm32"))]
1591 let georeferencing = rayon::in_place_scope(|scope| {
1592 scope.spawn(move |_| drop(decoder));
1593 scope.spawn(move |_| drop(item_dedup_cache));
1594 extract_georeferencing()
1595 });
1596 #[cfg(target_arch = "wasm32")]
1597 let georeferencing = extract_georeferencing();
1598
1599 ProcessingResult {
1600 meshes,
1601 instances,
1602 mesh_coordinate_space: coord_space,
1603 frame,
1604 site_transform,
1605 building_transform,
1606 metadata: ModelMetadata {
1607 schema_version,
1608 entity_count: total_entities,
1609 geometry_entity_count,
1610 coordinate_info: CoordinateInfo {
1611 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1612 is_geo_referenced: has_rtc_offset,
1613 },
1614 length_unit_scale: Some(unit_scale),
1615 georeferencing,
1616 },
1617 stats: ProcessingStats {
1618 total_meshes,
1619 total_vertices,
1620 total_triangles,
1621 parse_time_ms: parse_time.as_millis() as u64,
1622 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1623 lookup_time_ms: lookup_time.as_millis() as u64,
1624 preprocess_time_ms: preprocess_time.as_millis() as u64,
1625 geometry_time_ms: geometry_time.as_millis() as u64,
1626 total_time_ms: total_time.as_millis() as u64,
1627 from_cache: false,
1628 total_csg_failures: total_csg_failures as u64,
1629 products_with_failures,
1630 degenerate_triangles_dropped: backstop_dropped,
1631 point_cache_hits,
1632 point_cache_misses,
1633 faceted_brep_time_ms,
1634 geometry_diagnostics,
1635 },
1636 }
1637}
1638
1639