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};
33mod id_filter;
34pub use id_filter::{
35 process_geometry_filtered_with_quality, process_geometry_filtered_with_quality_and_ids,
36};
37pub(crate) mod instancing;
38mod jobs;
39mod opening_filter;
40mod properties;
41mod quick_metadata;
42mod schema_detection;
43mod site_local;
44
45pub use quick_metadata::is_quick_spatial_type_ci;
46pub use site_local::{convert_mesh_to_site_local, native_to_baked};
47pub(crate) use site_local::site_local_rotation_invalidates_captured_transforms;
48
49use jobs::{build_color_updates_for_jobs, process_entity_job};
50
51pub(crate) use color_layer::resolve_element_color_for_product_definition_shape;
52use color_layer::{
53 collect_presentation_layer_assignments,
54 resolve_presentation_layer_for_product_definition_shape,
55};
56use opening_filter::apply_opening_filter;
57use properties::resolve_space_zone_properties_lazy;
58use quick_metadata::{
59 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
60 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
61};
62use crate::mesh_frame::{MeshCoordinateSpace, MeshFrame};
63
64#[cfg(not(target_arch = "wasm32"))]
71use std::time::Instant as Clock;
72
73#[cfg(target_arch = "wasm32")]
74#[derive(Clone, Copy)]
75struct Clock;
76
77#[cfg(target_arch = "wasm32")]
78impl Clock {
79 #[inline]
80 fn now() -> Self {
81 Clock
82 }
83 #[inline]
84 fn elapsed(&self) -> std::time::Duration {
85 std::time::Duration::ZERO
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum OpeningFilterMode {
93 #[default]
95 Default = 0,
96 IgnoreAll = 1,
98 IgnoreOpaque = 2,
100}
101
102impl OpeningFilterMode {
103 pub fn cache_key_suffix(&self) -> &'static str {
106 match self {
107 Self::Default => "default",
108 Self::IgnoreAll => "ignore_all",
109 Self::IgnoreOpaque => "ignore_opaque",
110 }
111 }
112}
113
114pub struct ProcessingResult {
116 pub meshes: Vec<MeshData>,
117 pub instances: Vec<InstanceRecord>,
124 pub mesh_coordinate_space: MeshCoordinateSpace,
130 pub frame: MeshFrame,
139 pub site_transform: Option<Vec<f64>>,
141 pub building_transform: Option<Vec<f64>>,
143 pub metadata: ModelMetadata,
144 pub stats: ProcessingStats,
145}
146
147#[derive(Debug, Clone)]
151pub struct StreamingOptions {
152 pub initial_batch_size: usize,
154 pub throughput_batch_size: usize,
156 pub fast_first_batch: bool,
158 pub include_properties: bool,
160 pub include_presentation_layers: bool,
162 pub emit_quick_metadata_bootstrap: bool,
164 pub retain_emitted_meshes: bool,
166 pub tessellation_quality: TessellationQuality,
173 pub entity_index: Option<Arc<EntityIndex>>,
181 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
187 pub enable_instancing: bool,
196}
197
198impl Default for StreamingOptions {
199 fn default() -> Self {
200 Self {
201 initial_batch_size: 50,
202 throughput_batch_size: 50,
203 fast_first_batch: false,
204 include_properties: true,
205 include_presentation_layers: true,
206 emit_quick_metadata_bootstrap: false,
207 retain_emitted_meshes: true,
208 tessellation_quality: TessellationQuality::default(),
209 entity_index: None,
210 cancel: None,
211 enable_instancing: false,
212 }
213 }
214}
215
216pub(super) struct EntityJob {
218 pub(super) id: u32,
219 pub(super) ifc_type: IfcType,
220 pub(super) start: usize,
221 pub(super) end: usize,
222 pub(super) product_definition_shape_id: Option<u32>,
223 pub(super) element_color: [f32; 4],
224 pub(super) global_id: Option<String>,
225 pub(super) name: Option<String>,
226 pub(super) presentation_layer: Option<String>,
227 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
228 pub(super) representation_map_id: Option<u32>,
232}
233
234#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
236#[allow(clippy::too_many_arguments)]
238fn populate_entity_job_metadata(
239 job: &mut EntityJob,
240 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
241 element_material_color: &FxHashMap<u32, [f32; 4]>,
242 layer_by_assigned_representation: &FxHashMap<u32, String>,
243 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
244 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
245 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
246 decoder: &mut EntityDecoder,
247 include_presentation_layers: bool,
248) {
249 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
250 return;
251 }
252
253 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
254 return;
255 };
256
257 job.global_id = normalize_optional_string(entity.get_string(0));
258 job.name = normalize_optional_string(entity.get_string(2));
259 job.product_definition_shape_id = entity.get_ref(6);
260
261 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
262 return;
263 };
264
265 let resolved_color = color_cache_by_product_definition_shape
266 .entry(product_definition_shape_id)
267 .or_insert_with(|| {
268 resolve_element_color_for_product_definition_shape(
269 product_definition_shape_id,
270 geometry_style_index,
271 decoder,
272 )
273 });
274 if let Some(color) = resolved_color {
275 job.element_color = *color;
276 } else if let Some(color) = element_material_color.get(&job.id) {
277 job.element_color = *color;
278 }
279
280 if include_presentation_layers {
281 let resolved_layer = layer_cache_by_product_definition_shape
282 .entry(product_definition_shape_id)
283 .or_insert_with(|| {
284 resolve_presentation_layer_for_product_definition_shape(
285 product_definition_shape_id,
286 layer_by_assigned_representation,
287 layer_cache_by_representation,
288 decoder,
289 )
290 });
291 job.presentation_layer = resolved_layer.clone();
292 }
293}
294
295use crate::style::GeometryStyleInfo;
299
300pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
301 let value = raw?.trim();
302 if value.is_empty() || value == "$" {
303 return None;
304 }
305 Some(value.to_string())
306}
307
308fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
309 match ifc_type {
310 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
311 IfcType::IfcSlab => 95,
312 IfcType::IfcColumn => 90,
313 IfcType::IfcBeam => 85,
314 IfcType::IfcRoof => 80,
315 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
316 IfcType::IfcCurtainWall => 70,
317 IfcType::IfcFooting | IfcType::IfcPile => 65,
318 IfcType::IfcDoor | IfcType::IfcWindow => 30,
319 IfcType::IfcFurnishingElement => 10,
320 _ => 50,
321 }
322}
323
324pub fn process_geometry<T>(content: &T) -> ProcessingResult
326where
327 T: AsRef<[u8]> + ?Sized,
328{
329 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
330}
331
332pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
339where
340 T: AsRef<[u8]> + ?Sized,
341{
342 process_geometry_streaming_filtered_with_options(
343 content.as_ref(),
344 OpeningFilterMode::Default,
345 StreamingOptions {
346 initial_batch_size: usize::MAX,
347 throughput_batch_size: usize::MAX,
348 entity_index: Some(index),
349 ..StreamingOptions::default()
350 },
351 |_, _, _| {},
352 |_| {},
353 |_| {},
354 )
355}
356
357pub fn process_geometry_streaming(
359 content: &[u8],
360 batch_size: usize,
361 on_batch: impl FnMut(&[MeshData], usize, usize),
362) -> ProcessingResult {
363 process_geometry_streaming_with_options(
364 content,
365 StreamingOptions {
366 initial_batch_size: batch_size,
367 throughput_batch_size: batch_size,
368 ..StreamingOptions::default()
369 },
370 on_batch,
371 |_| {},
372 )
373}
374
375pub fn process_geometry_streaming_with_options(
377 content: &[u8],
378 options: StreamingOptions,
379 on_batch: impl FnMut(&[MeshData], usize, usize),
380 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
381) -> ProcessingResult {
382 process_geometry_streaming_with_options_and_bootstrap(
383 content,
384 options,
385 on_batch,
386 on_color_update,
387 |_| {},
388 )
389}
390
391pub fn process_geometry_streaming_with_options_and_bootstrap(
394 content: &[u8],
395 options: StreamingOptions,
396 on_batch: impl FnMut(&[MeshData], usize, usize),
397 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
398 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
399) -> ProcessingResult {
400 process_geometry_streaming_filtered_with_options(
401 content,
402 OpeningFilterMode::Default,
403 options,
404 on_batch,
405 on_color_update,
406 on_quick_metadata_bootstrap,
407 )
408}
409
410pub fn process_geometry_filtered<T>(
412 content: &T,
413 opening_filter: OpeningFilterMode,
414) -> ProcessingResult
415where
416 T: AsRef<[u8]> + ?Sized,
417{
418 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
419}
420
421pub fn process_geometry_streaming_filtered(
423 content: &[u8],
424 opening_filter: OpeningFilterMode,
425 batch_size: usize,
426 on_batch: impl FnMut(&[MeshData], usize, usize),
427 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
428) -> ProcessingResult {
429 process_geometry_streaming_filtered_with_options(
430 content,
431 opening_filter,
432 StreamingOptions {
433 initial_batch_size: batch_size,
434 throughput_batch_size: batch_size,
435 ..StreamingOptions::default()
436 },
437 on_batch,
438 on_color_update,
439 |_| {},
440 )
441}
442
443pub fn process_geometry_streaming_filtered_with_options(
445 content: &[u8],
446 opening_filter: OpeningFilterMode,
447 options: StreamingOptions,
448 on_batch: impl FnMut(&[MeshData], usize, usize),
449 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
450 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
451) -> ProcessingResult {
452 process_geometry_streaming_filtered_with_options_and_ids(
453 content,
454 opening_filter,
455 options,
456 None,
457 on_batch,
458 on_color_update,
459 on_quick_metadata_bootstrap,
460 )
461}
462
463fn process_geometry_streaming_filtered_with_options_and_ids(
464 content: &[u8],
465 opening_filter: OpeningFilterMode,
466 options: StreamingOptions,
467 entity_ids: Option<&HashSet<u32>>,
468 mut on_batch: impl FnMut(&[MeshData], usize, usize),
469 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
470 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
471) -> ProcessingResult {
472 let total_start = Clock::now();
473 let parse_start = Clock::now();
474 let entity_scan_start = Clock::now();
475
476 let pipeline_span = tracing::info_span!(
483 "geometry_pipeline",
484 byte_size = content.len(),
485 element_count = tracing::field::Empty,
486 total_ms = tracing::field::Empty,
487 );
488 let _pipeline_guard = pipeline_span.clone().entered();
489
490 tracing::info!(
491 content_size = content.len(),
492 "Starting IFC geometry processing"
493 );
494
495 let scan_span = tracing::info_span!(
496 "scan_prepass",
497 total_entities = tracing::field::Empty,
498 geometry_entities = tracing::field::Empty,
499 phase_ms = tracing::field::Empty,
500 );
501 let scan_guard = scan_span.clone().entered();
502
503 let provided_index = options.entity_index.clone();
510 let building_index = provided_index.is_none();
511 let mut inline_index = IndexBuilder::new(content.len(), building_index);
512 let mut decoder = match &provided_index {
513 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
514 None => EntityDecoder::new(content),
515 };
516 tracing::debug!("Entity index will be built inline during the scan");
517
518 let mut prepass_spans = crate::prepass::PrepassSpans::default();
524 let mut project_id: Option<u32> = None;
525 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
526 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
533
534 let mut scanner = EntityScanner::new(content);
536 let mut georeferencing_candidates = Vec::new();
537 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
538 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
543 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
544 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
549 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
558 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
559 let mut quick_spatial_nodes =
560 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
561 let mut quick_aggregate_links = if quick_metadata_enabled {
562 Vec::<(u32, Vec<u32>)>::new()
563 } else {
564 Vec::new()
565 };
566 let mut quick_containment_links = if quick_metadata_enabled {
567 Vec::<(u32, Vec<u32>)>::new()
568 } else {
569 Vec::new()
570 };
571 let mut quick_referenced_links = if quick_metadata_enabled {
576 Vec::<(u32, Vec<u32>)>::new()
577 } else {
578 Vec::new()
579 };
580 let mut quick_element_summaries = if quick_metadata_enabled {
581 HashMap::<u32, QuickMetadataEntitySummary>::new()
582 } else {
583 HashMap::new()
584 };
585 let mut total_entities = 0usize;
586 let mut site_entity_pos: Option<(usize, usize)> = None;
587 let mut building_entity_pos: Option<(usize, usize)> = None;
588
589 let defer_style_updates = options.fast_first_batch
590 && opening_filter == OpeningFilterMode::Default
591 && !options.include_presentation_layers;
592
593 while let Some((id, type_name, start, end)) = scanner.next_entity() {
594 total_entities += 1;
595 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
596 georeferencing_candidates.push((id, ifc_type));
597 }
598 if building_index {
599 inline_index.insert(id, (start, end));
600 }
601 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
602 if is_quick_spatial_type_ci(type_name) {
604 let args = parse_step_arguments(&content[start..end]);
605 let fallback = format!("{type_name} #{id}");
606 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
607 express_id: id,
608 type_name: type_name.to_string(),
609 name: extract_name_from_args(&args, &fallback),
610 elevation: if keyword_eq(type_name, "IFCBUILDINGSTOREY") {
611 extract_storey_elevation_from_args(&args)
612 } else {
613 None
614 },
615 children: Vec::new(),
616 contained: Vec::new(),
617 elements: Vec::new(),
618 named_as_child: false,
619 });
620 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
621 let args = parse_step_arguments(&content[start..end]);
622 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
623 quick_aggregate_links.push((
624 parent_id,
625 args.get(5)
626 .map(|token| parse_step_ref_list(token))
627 .unwrap_or_default(),
628 ));
629 }
630 } else if keyword_eq(type_name, "IFCRELCONTAINEDINSPATIALSTRUCTURE") {
631 let args = parse_step_arguments(&content[start..end]);
632 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
633 quick_containment_links.push((
634 parent_id,
635 args.get(4)
636 .map(|token| parse_step_ref_list(token))
637 .unwrap_or_default(),
638 ));
639 }
640 } else if keyword_eq(type_name, "IFCRELREFERENCEDINSPATIALSTRUCTURE") {
641 let args = parse_step_arguments(&content[start..end]);
642 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
643 quick_referenced_links.push((
644 parent_id,
645 args.get(4)
646 .map(|token| parse_step_ref_list(token))
647 .unwrap_or_default(),
648 ));
649 }
650 }
651 }
652
653 if keyword_eq(type_name, "IFCINDEXEDCOLOURMAP") {
654 prepass_spans.indexed_colour_maps.push((id, start, end));
656 continue;
657 }
658
659 if keyword_eq(type_name, "IFCSTYLEDITEM") {
660 prepass_spans.styled_items.push((id, start, end));
664 continue;
665 } else if keyword_eq(type_name, "IFCMATERIALDEFINITIONREPRESENTATION") {
666 prepass_spans.material_def_reprs.push((id, start, end));
667 continue;
668 } else if keyword_eq(type_name, "IFCRELASSOCIATESMATERIAL") {
669 prepass_spans.rel_associates_material.push((id, start, end));
670 continue;
671 } else if keyword_eq(type_name, "IFCPRESENTATIONLAYERASSIGNMENT") {
672 if !options.include_presentation_layers {
673 continue;
674 }
675 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
676 collect_presentation_layer_assignments(
677 &mut presentation_layer_by_assigned_id,
678 &layer_assignment,
679 );
680 }
681 continue;
682 } else if keyword_eq(type_name, "IFCPROPERTYSET") {
683 continue;
686 } else if keyword_eq(type_name, "IFCRELDEFINESBYPROPERTIES") {
687 if options.include_properties {
688 rel_defines_spans.push((start, end));
689 }
690 continue;
691 } else if keyword_starts_with(type_name, "IFCPROPERTY") {
692 continue;
695 } else if keyword_eq(type_name, "IFCRELVOIDSELEMENT") {
696 prepass_spans.void_rels.push((id, start, end));
697 } else if keyword_eq(type_name, "IFCRELFILLSELEMENT") {
698 prepass_spans.fills_rels.push((id, start, end));
699 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
700 prepass_spans.aggregate_rels.push((id, start, end));
705 } else if keyword_eq(type_name, "IFCPROJECT") && project_id.is_none() {
706 project_id = Some(id);
707 } else if keyword_eq(type_name, "IFCSITE") && site_entity_pos.is_none() {
708 site_entity_pos = Some((start, end));
709 } else if keyword_eq(type_name, "IFCBUILDING") && building_entity_pos.is_none() {
710 building_entity_pos = Some((start, end));
711 }
712
713 let (has_geometry, representationless_spatial) =
714 ifc_lite_core::geometry_flags_by_name(type_name);
715 if has_geometry {
716 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
720 if quick_metadata_enabled {
721 quick_element_summaries.insert(
722 id,
723 QuickMetadataEntitySummary {
724 express_id: id,
725 type_name: type_name.to_string(),
726 name: format!("{type_name} #{id}"),
727 global_id: None,
728 kind: "element".to_string(),
729 has_children: false,
730 element_count: None,
731 elevation: None,
732 },
733 );
734 }
735 entity_jobs.push(EntityJob {
736 id,
737 ifc_type,
738 start,
739 end,
740 product_definition_shape_id: None,
741 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
742 global_id: None,
743 name: None,
744 presentation_layer: None,
745 space_zone_properties: None,
746 representation_map_id: None,
747 });
748 } else if representationless_spatial
749 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
750 {
751 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
763 entity_jobs.push(EntityJob {
764 id,
765 ifc_type,
766 start,
767 end,
768 product_definition_shape_id: None,
769 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
770 global_id: None,
771 name: None,
772 presentation_layer: None,
773 space_zone_properties: None,
774 representation_map_id: None,
775 });
776 }
777 else if keyword_eq(type_name, "IFCMAPPEDITEM") {
784 let args = parse_step_arguments(&content[start..end]);
785 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
786 referenced_representation_maps.insert(source_id);
787 if options.enable_instancing {
791 mapped_item_plan
792 .entry(source_id)
793 .and_modify(|(count, template)| {
794 *count += 1;
795 if id < *template {
796 *template = id;
797 }
798 })
799 .or_insert((1, id));
800 }
801 }
802 } else if keyword_eq(type_name, "IFCRELDEFINESBYTYPE") {
803 let args = parse_step_arguments(&content[start..end]);
806 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
807 instantiated_type_ids.insert(type_id);
808 }
809 prepass_spans.defines_by_type.push((id, start, end));
811 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
812 let args = parse_step_arguments(&content[start..end]);
813 let rep_map_ids = args
815 .get(6)
816 .map(|token| parse_step_ref_list(token))
817 .unwrap_or_default();
818 if !rep_map_ids.is_empty() {
819 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
820 }
821 }
822 }
823
824 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
826
827 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
835 for (rep_map_id, _class) in crate::element::plan_type_geometry(
840 rep_map_ids,
841 &referenced_representation_maps,
842 instantiated_type_ids.contains(type_id),
843 crate::element::TypeGeometryMode::SuppressInstanced,
844 ) {
845 entity_jobs.push(EntityJob {
846 id: *type_id,
847 ifc_type: *ifc_type,
848 start: *start,
849 end: *end,
850 product_definition_shape_id: None,
851 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
852 global_id: None,
853 name: None,
854 presentation_layer: None,
855 space_zone_properties: None,
856 representation_map_id: Some(rep_map_id),
857 });
858 }
859 }
860
861 let entity_index = match provided_index {
866 Some(idx) => ProcessingIndex::Hash(idx),
867 None => inline_index.finish(),
868 };
869 entity_index.install(&mut decoder);
870
871 let resolved = crate::prepass::resolve_prepass(
876 &prepass_spans,
877 &mut decoder,
878 crate::prepass::ResolveOptions {
879 collect_indexed_colour_full: true,
880 defer_attached_styles: defer_style_updates,
881 },
882 );
883 let crate::prepass::ResolvedPrepass {
884 mut geometry_style_index,
885 indexed_colour_index,
886 indexed_colour_full,
887 element_material_colors,
888 void_index,
889 filling_by_opening,
890 deferred_attached_styled_spans: deferred_styled_item_positions,
891 ..
892 } = resolved;
893
894 let entity_scan_time = entity_scan_start.elapsed();
895 scan_span.record("total_entities", total_entities as u64);
896 scan_span.record("geometry_entities", entity_jobs.len() as u64);
897 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
898 drop(scan_guard);
899
900 let lookup_start = Clock::now();
901 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
902 if options.include_properties {
903 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
904 }
905 if options.fast_first_batch {
906 entity_jobs.sort_by(|left, right| {
907 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
908 });
909 }
910 let lookup_time = lookup_start.elapsed();
911 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
912 drop(lookup_span);
913
914 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
918 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
919 &entity_jobs,
920 &void_index,
921 &filling_by_opening,
922 &geometry_style_index,
923 &element_material_colors,
924 &mut decoder,
925 opening_filter,
926 );
927
928 if let Some(ids) = entity_ids {
934 entity_jobs.retain(|job| ids.contains(&job.id));
935 }
936
937 let schema_version = schema_detection::detect_schema_version(content).to_string();
938
939 let geometry_entity_count = entity_jobs.len();
940 tracing::info!(
941 total_entities = total_entities,
942 geometry_entities = geometry_entity_count,
943 voids = void_index.len(),
944 schema_version = %schema_version,
945 "Entity scanning complete"
946 );
947
948 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
949 for (parent_id, child_ids) in quick_aggregate_links {
950 if !spatial_nodes.contains_key(&parent_id) {
951 continue;
952 }
953 for child_id in child_ids {
954 if !spatial_nodes.contains_key(&child_id) {
955 continue;
956 }
957 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
958 parent.children.push(child_id);
959 }
960 if let Some(child) = spatial_nodes.get_mut(&child_id) {
961 child.named_as_child = true;
962 }
963 }
964 }
965 for (parent_id, element_ids) in quick_containment_links {
966 if !spatial_nodes.contains_key(&parent_id) {
967 continue;
968 }
969 for child_id in element_ids {
970 if spatial_nodes.contains_key(&child_id) {
983 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
984 parent.contained.push(child_id);
985 }
986 if let Some(child) = spatial_nodes.get_mut(&child_id) {
987 child.named_as_child = true;
988 }
989 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
990 parent.elements.push(child_id);
991 }
992 }
993 }
994 for (parent_id, element_ids) in quick_referenced_links {
998 if !spatial_nodes.contains_key(&parent_id) {
999 continue;
1000 }
1001 for child_id in element_ids {
1002 if spatial_nodes.contains_key(&child_id) {
1005 continue;
1006 }
1007 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1008 parent.elements.push(child_id);
1009 }
1010 }
1011 }
1012 let root_id = spatial_nodes
1016 .values()
1017 .filter(|node| keyword_eq(&node.type_name, "IFCPROJECT"))
1018 .map(|node| node.express_id)
1019 .min()
1020 .or_else(|| {
1021 spatial_nodes
1025 .values()
1026 .min_by_key(|node| (node.named_as_child, node.express_id))
1027 .map(|node| node.express_id)
1028 });
1029 let (spatial_tree, pruned_aggregate_edges) = root_id
1030 .and_then(|root| {
1031 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries).ok()
1032 })
1033 .unzip();
1034 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1035 schema_version: schema_version.clone(),
1036 entity_count: total_entities,
1037 spatial_tree,
1038 pruned_aggregate_edges: pruned_aggregate_edges.unwrap_or_default(),
1039 });
1040 }
1041
1042 let preprocess_start = Clock::now();
1044 let preprocess_span =
1045 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1046 let unit_scales = tracing::debug_span!("unit_scale")
1052 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1053 tracing::debug!(
1054 length_unit_scale = unit_scales.length_unit_scale,
1055 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1056 "Resolved unit scales"
1057 );
1058 decoder.seed_unit_scales(
1059 unit_scales.length_unit_scale,
1060 unit_scales.plane_angle_to_radians,
1061 );
1062 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1064 router.set_tessellation_quality(options.tessellation_quality);
1065 let material_spans = &prepass_spans.rel_associates_material;
1069 router.set_material_layer_index(Arc::new(
1070 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1071 ));
1072
1073 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1075 let entity = decoder.decode_at(start, end).ok()?;
1076 let matrix = router
1077 .resolve_scaled_placement(&entity, &mut decoder)
1078 .ok()?;
1079 Some(matrix.to_vec())
1080 });
1081 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1082 let entity = decoder.decode_at(start, end).ok()?;
1083 let matrix = router
1084 .resolve_scaled_placement(&entity, &mut decoder)
1085 .ok()?;
1086 Some(matrix.to_vec())
1087 });
1088
1089 let detected_rtc_offset = router.detect_rtc_offset_for_file(content, &mut decoder);
1094
1095 let frame = MeshFrame::select(site_transform.as_deref(), detected_rtc_offset);
1098 let coord_space = frame.coordinate_space();
1099 let has_rtc_offset = frame.needs_shift();
1100 router.set_rtc_offset(frame.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 let mut geometry_style_index = Arc::new(geometry_style_index);
1130 let indexed_colour_full = Arc::new(indexed_colour_full);
1131 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1136 content,
1137 &mut decoder,
1138 ));
1139 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1143 .iter()
1144 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1145 .collect();
1146 let element_material_colors = Arc::new(element_material_colors);
1147
1148 let total_jobs = entity_jobs.len();
1149 let initial_chunk_size = options.initial_batch_size.max(1);
1150 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1151 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1152 FxHashMap::default();
1153 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1154 FxHashMap::default();
1155 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1156 let mut meshes: Vec<MeshData> = Vec::new();
1157 let mut processed_jobs = 0usize;
1158 let mut total_meshes = 0usize;
1159 let mut total_vertices = 0usize;
1160 let mut total_triangles = 0usize;
1161 let mut chunk_start = 0usize;
1162 let mut current_chunk_size = initial_chunk_size;
1163
1164 let mut deferred_styles_applied = !defer_style_updates;
1165
1166 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1169
1170 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1175 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1176
1177 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1181
1182 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1212 && options.retain_emitted_meshes
1213 && coord_space != MeshCoordinateSpace::SiteLocal)
1214 .then(|| {
1215 Arc::new(
1216 mapped_item_plan
1217 .into_iter()
1218 .filter(|(_, (count, _))| *count >= 2)
1219 .collect::<FxHashMap<u32, (u32, u32)>>(),
1220 )
1221 });
1222 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1241 && !indexed_colour_full.is_empty())
1242 .then(|| {
1243 let ids: FxHashSet<u32> = indexed_colour_full
1244 .iter()
1245 .filter(|(_, m)| m.has_multiple_colours())
1246 .map(|(&id, _)| id)
1247 .collect();
1248 (!ids.is_empty()).then(|| Arc::new(ids))
1249 })
1250 .flatten();
1251 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1254 std::sync::Mutex::new(Vec::new());
1255
1256 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1264 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1265 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1266
1267 let worker_point_caches = jobs::new_worker_point_caches();
1268 let worker_placement_caches = jobs::new_worker_placement_caches();
1272
1273 let geometry_span = tracing::info_span!(
1274 "geometry",
1275 element_count = total_jobs,
1276 mesh_count = tracing::field::Empty,
1277 triangle_count = tracing::field::Empty,
1278 backstop_count = tracing::field::Empty,
1279 total_csg_failures = tracing::field::Empty,
1280 phase_ms = tracing::field::Empty,
1281 );
1282 let geometry_guard = geometry_span.clone().entered();
1283
1284 while chunk_start < total_jobs {
1285 if options
1289 .cancel
1290 .as_ref()
1291 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1292 {
1293 break;
1294 }
1295 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1296 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1297
1298 #[cfg(not(target_arch = "wasm32"))]
1302 {
1303 let entity_index_for_meta = entity_index_arc.clone();
1305 jobs_chunk.par_iter_mut().for_each(|job| {
1306 if job.global_id.is_some()
1307 || job.name.is_some()
1308 || job.product_definition_shape_id.is_some()
1309 {
1310 return;
1311 }
1312 let local_decoder = entity_index_for_meta.decoder(content);
1313 let Ok(entity) = local_decoder.decode_at_uncached(job.start, job.end) else {
1315 return;
1316 };
1317 job.global_id = normalize_optional_string(entity.get_string(0));
1318 job.name = normalize_optional_string(entity.get_string(2));
1319 job.product_definition_shape_id = entity.get_ref(6);
1320 });
1321
1322 for job in jobs_chunk.iter_mut() {
1324 let Some(pds_id) = job.product_definition_shape_id else {
1325 continue;
1326 };
1327 let resolved_color = color_cache_by_product_definition_shape
1328 .entry(pds_id)
1329 .or_insert_with(|| {
1330 resolve_element_color_for_product_definition_shape(
1331 pds_id,
1332 &geometry_style_index,
1333 &mut decoder,
1334 )
1335 });
1336 if let Some(color) = resolved_color {
1337 job.element_color = *color;
1338 } else if let Some(color) = element_material_color.get(&job.id) {
1339 job.element_color = *color;
1342 }
1343 if options.include_presentation_layers {
1344 let resolved_layer = layer_cache_by_product_definition_shape
1345 .entry(pds_id)
1346 .or_insert_with(|| {
1347 resolve_presentation_layer_for_product_definition_shape(
1348 pds_id,
1349 &presentation_layer_by_assigned_id,
1350 &mut layer_cache_by_representation,
1351 &mut decoder,
1352 )
1353 });
1354 job.presentation_layer = resolved_layer.clone();
1355 }
1356 }
1357 }
1358
1359 #[cfg(target_arch = "wasm32")]
1361 for job in jobs_chunk.iter_mut() {
1362 populate_entity_job_metadata(
1363 job,
1364 &geometry_style_index,
1365 &element_material_color,
1366 &presentation_layer_by_assigned_id,
1367 &mut color_cache_by_product_definition_shape,
1368 &mut layer_cache_by_product_definition_shape,
1369 &mut layer_cache_by_representation,
1370 &mut decoder,
1371 options.include_presentation_layers,
1372 );
1373 }
1374 let site_local_rotation: Option<&Vec<f64>> =
1375 if coord_space == MeshCoordinateSpace::SiteLocal {
1376 site_transform.as_ref()
1377 } else {
1378 None
1379 };
1380 let chunk_meshes: Vec<MeshData> = jobs_chunk
1385 .par_iter()
1386 .map(|job| {
1387 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1388 let mut fallback_cache = FxHashMap::default();
1398 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1399 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1400 match slot_guard.as_deref_mut() {
1401 Some(cache) => cache,
1402 None => &mut fallback_cache,
1403 };
1404 let mut fallback_placement_cache = FxHashMap::default();
1413 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1414 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1415 match placement_slot_guard.as_deref_mut() {
1416 Some(cache) => cache,
1417 None => &mut fallback_placement_cache,
1418 };
1419 process_entity_job(
1420 job,
1421 content,
1422 &entity_index_arc,
1423 unit_scale,
1424 rtc_offset,
1425 seed_plane_angle_to_radians,
1426 options.tessellation_quality,
1427 void_index_arc.as_ref(),
1428 skipped_entity_ids.as_ref(),
1429 geometry_style_index.as_ref(),
1430 indexed_colour_full.as_ref(),
1431 element_material_colors.as_ref(),
1432 texture_index.as_ref(),
1433 site_local_rotation,
1434 &diag_collectors,
1435 &item_dedup_cache,
1436 &brep_signature_cache,
1437 &mapped_item_cache,
1438 instancing_plan.as_ref(),
1439 indexed_colour_split_ids.as_ref(),
1440 &raw_instance_collector,
1441 worker_point_cache,
1442 worker_placement_cache,
1443 &point_cache_hits_collector,
1444 &point_cache_misses_collector,
1445 &faceted_brep_ns_collector,
1446 )
1447 })
1448 .flatten_iter()
1449 .collect();
1450
1451 processed_jobs += jobs_chunk.len();
1452 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1453 total_triangles += chunk_meshes
1454 .iter()
1455 .map(|m| m.triangle_count())
1456 .sum::<usize>();
1457
1458 if !chunk_meshes.is_empty() {
1459 total_meshes += chunk_meshes.len();
1460 let emit_mesh_chunk_size = current_chunk_size.max(1);
1461 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1462 on_batch(emitted_meshes, processed_jobs, total_jobs);
1463 }
1464 if options.retain_emitted_meshes {
1465 meshes.extend(chunk_meshes);
1466 }
1467
1468 if !deferred_styles_applied {
1469 let mut rebuilt_styles = {
1474 let mut style_decoder =
1475 entity_index_arc.decoder(content);
1476 crate::prepass::resolve_styled_item_spans(
1477 &deferred_styled_item_positions,
1478 &mut style_decoder,
1479 )
1480 };
1481 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1482 geometry_style_index = Arc::new(rebuilt_styles);
1483 let deferred_color_updates = build_color_updates_for_jobs(
1484 &entity_jobs[..processed_jobs],
1485 geometry_style_index.as_ref(),
1486 content,
1487 &entity_index_arc,
1488 );
1489 if !deferred_color_updates.is_empty() {
1490 on_color_update(&deferred_color_updates);
1491 }
1492 deferred_styles_applied = true;
1493 }
1494 }
1495 chunk_start = chunk_end;
1496 current_chunk_size = throughput_chunk_size;
1497 }
1498
1499 let geometry_time = geometry_start.elapsed();
1500 let csg_failures = diag_collectors
1503 .csg_failures
1504 .into_inner()
1505 .unwrap_or_else(|poisoned| poisoned.into_inner());
1506 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1507 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1508 let backstop_dropped = diag_collectors.backstop.into_inner();
1509 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1511 let point_cache_hits = point_cache_hits_collector.into_inner();
1512 let point_cache_misses = point_cache_misses_collector.into_inner();
1513 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1514 geometry_span.record("mesh_count", total_meshes as u64);
1515 geometry_span.record("triangle_count", total_triangles as u64);
1516 geometry_span.record("backstop_count", backstop_dropped);
1517 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1518 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1519 drop(geometry_guard);
1520 if total_csg_failures > 0 {
1521 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1522 for fails in csg_failures.values() {
1523 for f in fails {
1524 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1525 }
1526 }
1527 let open_topology_accepted = *by_reason.get("KernelError").unwrap_or(&0);
1532 let dropped = total_csg_failures - open_topology_accepted;
1533 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1534 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1535 let breakdown = breakdown
1536 .iter()
1537 .map(|(reason, count)| format!("{reason}={count}"))
1538 .collect::<Vec<_>>()
1539 .join(" ");
1540 tracing::warn!(
1541 total_csg_failures,
1542 products_with_failures,
1543 dropped,
1544 open_topology_accepted,
1545 %breakdown,
1546 "{}",
1547 csg_summary::csg_summary_message(dropped, open_topology_accepted)
1548 );
1549 }
1550
1551 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1552 diagnostics::collate(
1553 diag_collectors.classification,
1554 diag_collectors.host_diags,
1555 diag_collectors.rect_fast,
1556 diag_collectors.unsupported_items,
1557 &csg_failures,
1558 oversized_ref_drops,
1559 )
1560 });
1561
1562 let instances = instancing::finalize_instances(
1567 raw_instance_collector
1568 .into_inner()
1569 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1570 &mut meshes,
1571 &mapped_item_cache,
1572 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1573 );
1574
1575 let total_time = total_start.elapsed();
1576 pipeline_span.record("element_count", total_jobs as u64);
1577 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1578
1579 tracing::info!(
1580 meshes = meshes.len(),
1581 instances = instances.len(),
1582 vertices = total_vertices,
1583 triangles = total_triangles,
1584 backstop_count = backstop_dropped,
1585 geometry_time_ms = geometry_time.as_millis(),
1586 total_time_ms = total_time.as_millis(),
1587 "Geometry processing complete"
1588 );
1589
1590 let extract_georeferencing = || crate::georeferencing::extract_georeferencing_from_candidates(
1591 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1592 );
1593 #[cfg(not(target_arch = "wasm32"))]
1597 let georeferencing = rayon::in_place_scope(|scope| {
1598 scope.spawn(move |_| drop(decoder));
1599 scope.spawn(move |_| drop(item_dedup_cache));
1600 extract_georeferencing()
1601 });
1602 #[cfg(target_arch = "wasm32")]
1603 let georeferencing = extract_georeferencing();
1604
1605 ProcessingResult {
1606 meshes,
1607 instances,
1608 mesh_coordinate_space: coord_space,
1609 frame,
1610 site_transform,
1611 building_transform,
1612 metadata: ModelMetadata {
1613 schema_version,
1614 entity_count: total_entities,
1615 geometry_entity_count,
1616 coordinate_info: CoordinateInfo {
1617 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1618 is_geo_referenced: has_rtc_offset,
1619 },
1620 length_unit_scale: Some(unit_scale),
1621 georeferencing,
1622 },
1623 stats: ProcessingStats {
1624 total_meshes,
1625 total_vertices,
1626 total_triangles,
1627 parse_time_ms: parse_time.as_millis() as u64,
1628 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1629 lookup_time_ms: lookup_time.as_millis() as u64,
1630 preprocess_time_ms: preprocess_time.as_millis() as u64,
1631 geometry_time_ms: geometry_time.as_millis() as u64,
1632 total_time_ms: total_time.as_millis() as u64,
1633 from_cache: false,
1634 total_csg_failures: total_csg_failures as u64,
1635 products_with_failures,
1636 degenerate_triangles_dropped: backstop_dropped,
1637 point_cache_hits,
1638 point_cache_misses,
1639 faceted_brep_time_ms,
1640 geometry_diagnostics,
1641 },
1642 }
1643}
1644
1645