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 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;
43
44use jobs::{build_color_updates_for_jobs, process_entity_job};
45
46use color_layer::{
47 collect_presentation_layer_assignments, resolve_element_color_for_product_definition_shape,
48 resolve_presentation_layer_for_product_definition_shape,
49};
50use opening_filter::apply_opening_filter;
51use properties::resolve_space_zone_properties_lazy;
52use quick_metadata::{
53 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
54 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
55};
56use site_local::{
57 translation_is_nonidentity, MODEL_RTC_MESH_COORDINATE_SPACE, RAW_IFC_MESH_COORDINATE_SPACE,
58 SITE_LOCAL_MESH_COORDINATE_SPACE,
59};
60
61#[cfg(not(target_arch = "wasm32"))]
68use std::time::Instant as Clock;
69
70#[cfg(target_arch = "wasm32")]
71#[derive(Clone, Copy)]
72struct Clock;
73
74#[cfg(target_arch = "wasm32")]
75impl Clock {
76 #[inline]
77 fn now() -> Self {
78 Clock
79 }
80 #[inline]
81 fn elapsed(&self) -> std::time::Duration {
82 std::time::Duration::ZERO
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum OpeningFilterMode {
90 #[default]
92 Default = 0,
93 IgnoreAll = 1,
95 IgnoreOpaque = 2,
97}
98
99impl OpeningFilterMode {
100 pub fn cache_key_suffix(&self) -> &'static str {
103 match self {
104 Self::Default => "default",
105 Self::IgnoreAll => "ignore_all",
106 Self::IgnoreOpaque => "ignore_opaque",
107 }
108 }
109}
110
111pub struct ProcessingResult {
113 pub meshes: Vec<MeshData>,
114 pub instances: Vec<InstanceRecord>,
121 pub mesh_coordinate_space: Option<String>,
123 pub site_transform: Option<Vec<f64>>,
125 pub building_transform: Option<Vec<f64>>,
127 pub metadata: ModelMetadata,
128 pub stats: ProcessingStats,
129}
130
131#[derive(Debug, Clone)]
135pub struct StreamingOptions {
136 pub initial_batch_size: usize,
138 pub throughput_batch_size: usize,
140 pub fast_first_batch: bool,
142 pub include_properties: bool,
144 pub include_presentation_layers: bool,
146 pub emit_quick_metadata_bootstrap: bool,
148 pub retain_emitted_meshes: bool,
150 pub tessellation_quality: TessellationQuality,
157 pub entity_index: Option<Arc<EntityIndex>>,
165 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
171 pub enable_instancing: bool,
180}
181
182impl Default for StreamingOptions {
183 fn default() -> Self {
184 Self {
185 initial_batch_size: 50,
186 throughput_batch_size: 50,
187 fast_first_batch: false,
188 include_properties: true,
189 include_presentation_layers: true,
190 emit_quick_metadata_bootstrap: false,
191 retain_emitted_meshes: true,
192 tessellation_quality: TessellationQuality::default(),
193 entity_index: None,
194 cancel: None,
195 enable_instancing: false,
196 }
197 }
198}
199
200pub(super) struct EntityJob {
202 pub(super) id: u32,
203 pub(super) ifc_type: IfcType,
204 pub(super) start: usize,
205 pub(super) end: usize,
206 pub(super) product_definition_shape_id: Option<u32>,
207 pub(super) element_color: [f32; 4],
208 pub(super) global_id: Option<String>,
209 pub(super) name: Option<String>,
210 pub(super) presentation_layer: Option<String>,
211 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
212 pub(super) representation_map_id: Option<u32>,
216}
217
218#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
220#[allow(clippy::too_many_arguments)]
222fn populate_entity_job_metadata(
223 job: &mut EntityJob,
224 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
225 element_material_color: &FxHashMap<u32, [f32; 4]>,
226 layer_by_assigned_representation: &FxHashMap<u32, String>,
227 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
228 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
229 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
230 decoder: &mut EntityDecoder,
231 include_presentation_layers: bool,
232) {
233 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
234 return;
235 }
236
237 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
238 return;
239 };
240
241 job.global_id = normalize_optional_string(entity.get_string(0));
242 job.name = normalize_optional_string(entity.get_string(2));
243 job.product_definition_shape_id = entity.get_ref(6);
244
245 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
246 return;
247 };
248
249 let resolved_color = color_cache_by_product_definition_shape
250 .entry(product_definition_shape_id)
251 .or_insert_with(|| {
252 resolve_element_color_for_product_definition_shape(
253 product_definition_shape_id,
254 geometry_style_index,
255 decoder,
256 )
257 });
258 if let Some(color) = resolved_color {
259 job.element_color = *color;
260 } else if let Some(color) = element_material_color.get(&job.id) {
261 job.element_color = *color;
262 }
263
264 if include_presentation_layers {
265 let resolved_layer = layer_cache_by_product_definition_shape
266 .entry(product_definition_shape_id)
267 .or_insert_with(|| {
268 resolve_presentation_layer_for_product_definition_shape(
269 product_definition_shape_id,
270 layer_by_assigned_representation,
271 layer_cache_by_representation,
272 decoder,
273 )
274 });
275 job.presentation_layer = resolved_layer.clone();
276 }
277}
278
279use crate::style::GeometryStyleInfo;
283
284pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
286 let list = entity.get_list(index)?;
287 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
288 if refs.is_empty() {
289 None
290 } else {
291 Some(refs)
292 }
293}
294
295pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
296 let value = raw?.trim();
297 if value.is_empty() || value == "$" {
298 return None;
299 }
300 Some(value.to_string())
301}
302
303fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
304 match ifc_type {
305 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
306 IfcType::IfcSlab => 95,
307 IfcType::IfcColumn => 90,
308 IfcType::IfcBeam => 85,
309 IfcType::IfcRoof => 80,
310 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
311 IfcType::IfcCurtainWall => 70,
312 IfcType::IfcFooting | IfcType::IfcPile => 65,
313 IfcType::IfcDoor | IfcType::IfcWindow => 30,
314 IfcType::IfcFurnishingElement => 10,
315 _ => 50,
316 }
317}
318
319pub fn process_geometry<T>(content: &T) -> ProcessingResult
321where
322 T: AsRef<[u8]> + ?Sized,
323{
324 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
325}
326
327pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
334where
335 T: AsRef<[u8]> + ?Sized,
336{
337 process_geometry_streaming_filtered_with_options(
338 content.as_ref(),
339 OpeningFilterMode::Default,
340 StreamingOptions {
341 initial_batch_size: usize::MAX,
342 throughput_batch_size: usize::MAX,
343 entity_index: Some(index),
344 ..StreamingOptions::default()
345 },
346 |_, _, _| {},
347 |_| {},
348 |_| {},
349 )
350}
351
352pub fn process_geometry_streaming(
354 content: &[u8],
355 batch_size: usize,
356 on_batch: impl FnMut(&[MeshData], usize, usize),
357) -> ProcessingResult {
358 process_geometry_streaming_with_options(
359 content,
360 StreamingOptions {
361 initial_batch_size: batch_size,
362 throughput_batch_size: batch_size,
363 ..StreamingOptions::default()
364 },
365 on_batch,
366 |_| {},
367 )
368}
369
370pub fn process_geometry_streaming_with_options(
372 content: &[u8],
373 options: StreamingOptions,
374 on_batch: impl FnMut(&[MeshData], usize, usize),
375 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
376) -> ProcessingResult {
377 process_geometry_streaming_with_options_and_bootstrap(
378 content,
379 options,
380 on_batch,
381 on_color_update,
382 |_| {},
383 )
384}
385
386pub fn process_geometry_streaming_with_options_and_bootstrap(
389 content: &[u8],
390 options: StreamingOptions,
391 on_batch: impl FnMut(&[MeshData], usize, usize),
392 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
393 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
394) -> ProcessingResult {
395 process_geometry_streaming_filtered_with_options(
396 content,
397 OpeningFilterMode::Default,
398 options,
399 on_batch,
400 on_color_update,
401 on_quick_metadata_bootstrap,
402 )
403}
404
405pub fn process_geometry_filtered<T>(
407 content: &T,
408 opening_filter: OpeningFilterMode,
409) -> ProcessingResult
410where
411 T: AsRef<[u8]> + ?Sized,
412{
413 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
414}
415
416pub fn process_geometry_filtered_with_quality<T>(
420 content: &T,
421 opening_filter: OpeningFilterMode,
422 tessellation_quality: TessellationQuality,
423) -> ProcessingResult
424where
425 T: AsRef<[u8]> + ?Sized,
426{
427 let content = content.as_ref();
428 process_geometry_streaming_filtered_with_options(
429 content,
430 opening_filter,
431 StreamingOptions {
432 initial_batch_size: usize::MAX,
433 throughput_batch_size: usize::MAX,
434 tessellation_quality,
435 ..StreamingOptions::default()
436 },
437 |_, _, _| {},
438 |_| {},
439 |_| {},
440 )
441}
442
443pub fn process_geometry_streaming_filtered(
445 content: &[u8],
446 opening_filter: OpeningFilterMode,
447 batch_size: usize,
448 on_batch: impl FnMut(&[MeshData], usize, usize),
449 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
450) -> ProcessingResult {
451 process_geometry_streaming_filtered_with_options(
452 content,
453 opening_filter,
454 StreamingOptions {
455 initial_batch_size: batch_size,
456 throughput_batch_size: batch_size,
457 ..StreamingOptions::default()
458 },
459 on_batch,
460 on_color_update,
461 |_| {},
462 )
463}
464
465pub fn process_geometry_streaming_filtered_with_options(
467 content: &[u8],
468 opening_filter: OpeningFilterMode,
469 options: StreamingOptions,
470 mut on_batch: impl FnMut(&[MeshData], usize, usize),
471 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
472 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
473) -> ProcessingResult {
474 let total_start = Clock::now();
475 let parse_start = Clock::now();
476 let entity_scan_start = Clock::now();
477
478 let pipeline_span = tracing::info_span!(
485 "geometry_pipeline",
486 byte_size = content.len(),
487 element_count = tracing::field::Empty,
488 total_ms = tracing::field::Empty,
489 );
490 let _pipeline_guard = pipeline_span.clone().entered();
491
492 tracing::info!(
493 content_size = content.len(),
494 "Starting IFC geometry processing"
495 );
496
497 let scan_span = tracing::info_span!(
498 "scan_prepass",
499 total_entities = tracing::field::Empty,
500 geometry_entities = tracing::field::Empty,
501 phase_ms = tracing::field::Empty,
502 );
503 let scan_guard = scan_span.clone().entered();
504
505 let provided_index = options.entity_index.clone();
512 let building_index = provided_index.is_none();
513 let mut inline_index = IndexBuilder::new(content.len(), building_index);
514 let mut decoder = match &provided_index {
515 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
516 None => EntityDecoder::new(content),
517 };
518 tracing::debug!("Entity index will be built inline during the scan");
519
520 let mut prepass_spans = crate::prepass::PrepassSpans::default();
526 let mut project_id: Option<u32> = None;
527 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
528 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
535
536 let mut scanner = EntityScanner::new(content);
538 let mut georeferencing_candidates = Vec::new();
539 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
540 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
545 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
546 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
551 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
560 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
561 let mut quick_spatial_nodes =
562 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
563 let mut quick_aggregate_links = if quick_metadata_enabled {
564 Vec::<(u32, Vec<u32>)>::new()
565 } else {
566 Vec::new()
567 };
568 let mut quick_containment_links = if quick_metadata_enabled {
569 Vec::<(u32, Vec<u32>)>::new()
570 } else {
571 Vec::new()
572 };
573 let mut quick_referenced_links = if quick_metadata_enabled {
578 Vec::<(u32, Vec<u32>)>::new()
579 } else {
580 Vec::new()
581 };
582 let mut quick_element_summaries = if quick_metadata_enabled {
583 HashMap::<u32, QuickMetadataEntitySummary>::new()
584 } else {
585 HashMap::new()
586 };
587 let mut total_entities = 0usize;
588 let mut site_entity_pos: Option<(usize, usize)> = None;
589 let mut building_entity_pos: Option<(usize, usize)> = None;
590
591 let defer_style_updates = options.fast_first_batch
592 && opening_filter == OpeningFilterMode::Default
593 && !options.include_presentation_layers;
594
595 while let Some((id, type_name, start, end)) = scanner.next_entity() {
596 total_entities += 1;
597 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
598 georeferencing_candidates.push((id, ifc_type));
599 }
600 if building_index {
601 inline_index.insert(id, (start, end));
602 }
603 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
604 if is_quick_spatial_type_ci(type_name) {
606 let args = parse_step_arguments(&content[start..end]);
607 let fallback = format!("{type_name} #{id}");
608 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
609 express_id: id,
610 type_name: type_name.to_string(),
611 name: extract_name_from_args(&args, &fallback),
612 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
613 extract_storey_elevation_from_args(&args)
614 } else {
615 None
616 },
617 children: Vec::new(),
618 elements: Vec::new(),
619 parent: None,
620 });
621 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
622 let args = parse_step_arguments(&content[start..end]);
623 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
624 quick_aggregate_links.push((
625 parent_id,
626 args.get(5)
627 .map(|token| parse_step_ref_list(token))
628 .unwrap_or_default(),
629 ));
630 }
631 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
632 let args = parse_step_arguments(&content[start..end]);
633 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
634 quick_containment_links.push((
635 parent_id,
636 args.get(4)
637 .map(|token| parse_step_ref_list(token))
638 .unwrap_or_default(),
639 ));
640 }
641 } else if type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
642 let args = parse_step_arguments(&content[start..end]);
643 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
644 quick_referenced_links.push((
645 parent_id,
646 args.get(4)
647 .map(|token| parse_step_ref_list(token))
648 .unwrap_or_default(),
649 ));
650 }
651 }
652 }
653
654 if type_name == "IFCINDEXEDCOLOURMAP" {
655 prepass_spans.indexed_colour_maps.push((id, start, end));
657 continue;
658 }
659
660 if type_name == "IFCSTYLEDITEM" {
661 prepass_spans.styled_items.push((id, start, end));
665 continue;
666 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
667 prepass_spans.material_def_reprs.push((id, start, end));
668 continue;
669 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
670 prepass_spans.rel_associates_material.push((id, start, end));
671 continue;
672 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
673 if !options.include_presentation_layers {
674 continue;
675 }
676 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
677 collect_presentation_layer_assignments(
678 &mut presentation_layer_by_assigned_id,
679 &layer_assignment,
680 );
681 }
682 continue;
683 } else if type_name == "IFCPROPERTYSET" {
684 continue;
687 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
688 if options.include_properties {
689 rel_defines_spans.push((start, end));
690 }
691 continue;
692 } else if type_name.starts_with("IFCPROPERTY") {
693 continue;
696 } else if type_name == "IFCRELVOIDSELEMENT" {
697 prepass_spans.void_rels.push((id, start, end));
698 } else if type_name == "IFCRELFILLSELEMENT" {
699 prepass_spans.fills_rels.push((id, start, end));
700 } else if type_name == "IFCRELAGGREGATES" {
701 prepass_spans.aggregate_rels.push((id, start, end));
706 } else if type_name == "IFCPROJECT" && project_id.is_none() {
707 project_id = Some(id);
708 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
709 site_entity_pos = Some((start, end));
710 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
711 building_entity_pos = Some((start, end));
712 }
713
714 let (has_geometry, representationless_spatial) =
715 ifc_lite_core::geometry_flags_by_name(type_name);
716 if has_geometry {
717 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
721 if quick_metadata_enabled {
722 quick_element_summaries.insert(
723 id,
724 QuickMetadataEntitySummary {
725 express_id: id,
726 type_name: type_name.to_string(),
727 name: format!("{type_name} #{id}"),
728 global_id: None,
729 kind: "element".to_string(),
730 has_children: false,
731 element_count: None,
732 elevation: None,
733 },
734 );
735 }
736 entity_jobs.push(EntityJob {
737 id,
738 ifc_type,
739 start,
740 end,
741 product_definition_shape_id: None,
742 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
743 global_id: None,
744 name: None,
745 presentation_layer: None,
746 space_zone_properties: None,
747 representation_map_id: None,
748 });
749 } else if representationless_spatial
750 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
751 {
752 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
764 entity_jobs.push(EntityJob {
765 id,
766 ifc_type,
767 start,
768 end,
769 product_definition_shape_id: None,
770 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
771 global_id: None,
772 name: None,
773 presentation_layer: None,
774 space_zone_properties: None,
775 representation_map_id: None,
776 });
777 }
778 else if type_name == "IFCMAPPEDITEM" {
785 let args = parse_step_arguments(&content[start..end]);
786 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
787 referenced_representation_maps.insert(source_id);
788 if options.enable_instancing {
792 mapped_item_plan
793 .entry(source_id)
794 .and_modify(|(count, template)| {
795 *count += 1;
796 if id < *template {
797 *template = id;
798 }
799 })
800 .or_insert((1, id));
801 }
802 }
803 } else if type_name == "IFCRELDEFINESBYTYPE" {
804 let args = parse_step_arguments(&content[start..end]);
807 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
808 instantiated_type_ids.insert(type_id);
809 }
810 prepass_spans.defines_by_type.push((id, start, end));
812 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
813 let args = parse_step_arguments(&content[start..end]);
814 let rep_map_ids = args
816 .get(6)
817 .map(|token| parse_step_ref_list(token))
818 .unwrap_or_default();
819 if !rep_map_ids.is_empty() {
820 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
821 }
822 }
823 }
824
825 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
827
828 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
836 for (rep_map_id, _class) in crate::element::plan_type_geometry(
841 rep_map_ids,
842 &referenced_representation_maps,
843 instantiated_type_ids.contains(type_id),
844 crate::element::TypeGeometryMode::SuppressInstanced,
845 ) {
846 entity_jobs.push(EntityJob {
847 id: *type_id,
848 ifc_type: *ifc_type,
849 start: *start,
850 end: *end,
851 product_definition_shape_id: None,
852 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
853 global_id: None,
854 name: None,
855 presentation_layer: None,
856 space_zone_properties: None,
857 representation_map_id: Some(rep_map_id),
858 });
859 }
860 }
861
862 let entity_index = match provided_index {
867 Some(idx) => ProcessingIndex::Hash(idx),
868 None => inline_index.finish(),
869 };
870 entity_index.install(&mut decoder);
871
872 let resolved = crate::prepass::resolve_prepass(
877 &prepass_spans,
878 &mut decoder,
879 crate::prepass::ResolveOptions {
880 collect_indexed_colour_full: true,
881 defer_attached_styles: defer_style_updates,
882 },
883 );
884 let crate::prepass::ResolvedPrepass {
885 mut geometry_style_index,
886 indexed_colour_index,
887 indexed_colour_full,
888 element_material_colors,
889 void_index,
890 filling_by_opening,
891 deferred_attached_styled_spans: deferred_styled_item_positions,
892 ..
893 } = resolved;
894
895 let entity_scan_time = entity_scan_start.elapsed();
896 scan_span.record("total_entities", total_entities as u64);
897 scan_span.record("geometry_entities", entity_jobs.len() as u64);
898 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
899 drop(scan_guard);
900
901 let lookup_start = Clock::now();
902 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
903 if options.include_properties {
904 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
905 }
906 if options.fast_first_batch {
907 entity_jobs.sort_by(|left, right| {
908 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
909 });
910 }
911 let lookup_time = lookup_start.elapsed();
912 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
913 drop(lookup_span);
914
915 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
916 &entity_jobs,
917 &void_index,
918 &filling_by_opening,
919 &geometry_style_index,
920 &mut decoder,
921 opening_filter,
922 );
923
924 let schema_version = schema_detection::detect_schema_version(content).to_string();
925
926 let geometry_entity_count = entity_jobs.len();
927 tracing::info!(
928 total_entities = total_entities,
929 geometry_entities = geometry_entity_count,
930 voids = void_index.len(),
931 schema_version = %schema_version,
932 "Entity scanning complete"
933 );
934
935 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
936 for (parent_id, child_ids) in quick_aggregate_links {
937 if !spatial_nodes.contains_key(&parent_id) {
938 continue;
939 }
940 for child_id in child_ids {
941 if !spatial_nodes.contains_key(&child_id) {
942 continue;
943 }
944 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
945 parent.children.push(child_id);
946 }
947 if let Some(child) = spatial_nodes.get_mut(&child_id) {
948 child.parent = Some(parent_id);
949 }
950 }
951 }
952 for (parent_id, element_ids) in quick_containment_links {
953 if !spatial_nodes.contains_key(&parent_id) {
954 continue;
955 }
956 for child_id in element_ids {
957 if spatial_nodes.contains_key(&child_id) {
964 let already_placed = spatial_nodes
967 .get(&child_id)
968 .is_some_and(|child| child.parent.is_some());
969 if !already_placed {
970 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
971 parent.children.push(child_id);
972 }
973 if let Some(child) = spatial_nodes.get_mut(&child_id) {
974 child.parent = Some(parent_id);
975 }
976 }
977 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
978 parent.elements.push(child_id);
979 }
980 }
981 }
982 for (parent_id, element_ids) in quick_referenced_links {
986 if !spatial_nodes.contains_key(&parent_id) {
987 continue;
988 }
989 for child_id in element_ids {
990 if spatial_nodes.contains_key(&child_id) {
993 continue;
994 }
995 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
996 parent.elements.push(child_id);
997 }
998 }
999 }
1000 let mut root_id = spatial_nodes
1001 .values()
1002 .find(|node| node.type_name == "IfcProject")
1003 .map(|node| node.express_id);
1004 if root_id.is_none() {
1005 root_id = spatial_nodes
1006 .values()
1007 .find(|node| node.parent.is_none())
1008 .map(|node| node.express_id);
1009 }
1010 let spatial_tree = root_id
1011 .map(|root| {
1012 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1013 })
1014 .transpose()
1015 .unwrap_or(None);
1016 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1017 schema_version: schema_version.clone(),
1018 entity_count: total_entities,
1019 spatial_tree,
1020 });
1021 }
1022
1023 let preprocess_start = Clock::now();
1025 let preprocess_span =
1026 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1027 let unit_scales = tracing::debug_span!("unit_scale")
1033 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1034 tracing::debug!(
1035 length_unit_scale = unit_scales.length_unit_scale,
1036 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1037 "Resolved unit scales"
1038 );
1039 decoder.seed_unit_scales(
1040 unit_scales.length_unit_scale,
1041 unit_scales.plane_angle_to_radians,
1042 );
1043 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1045 router.set_tessellation_quality(options.tessellation_quality);
1046 let material_spans = &prepass_spans.rel_associates_material;
1050 router.set_material_layer_index(Arc::new(
1051 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1052 ));
1053
1054 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1056 let entity = decoder.decode_at(start, end).ok()?;
1057 let matrix = router
1058 .resolve_scaled_placement(&entity, &mut decoder)
1059 .ok()?;
1060 Some(matrix.to_vec())
1061 });
1062 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1063 let entity = decoder.decode_at(start, end).ok()?;
1064 let matrix = router
1065 .resolve_scaled_placement(&entity, &mut decoder)
1066 .ok()?;
1067 Some(matrix.to_vec())
1068 });
1069
1070 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1071 .iter()
1072 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1073 .collect();
1074 let detected_rtc_offset =
1075 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1076
1077 let site_rtc = site_transform
1086 .as_ref()
1087 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1089 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1090 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1091 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1092 } else if detected_has_offset {
1093 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1094 } else {
1095 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1096 };
1097 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1098 router.set_rtc_offset(rtc_offset);
1099 let preprocess_time = preprocess_start.elapsed();
1100 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1101 drop(preprocess_span);
1102
1103 let parse_time = parse_start.elapsed();
1104 tracing::info!(
1105 entity_scan_time_ms = entity_scan_time.as_millis(),
1106 lookup_time_ms = lookup_time.as_millis(),
1107 preprocess_time_ms = preprocess_time.as_millis(),
1108 parse_time_ms = parse_time.as_millis(),
1109 "Parse phase complete, starting geometry extraction"
1110 );
1111
1112 let geometry_start = Clock::now();
1114 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1116 let rtc_offset = router.rtc_offset();
1117 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1125 let void_index_arc = Arc::new(filtered_void_index);
1126 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1127 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1130 let mut geometry_style_index = Arc::new(geometry_style_index);
1131 let indexed_colour_full = Arc::new(indexed_colour_full);
1132 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1137 content,
1138 &mut decoder,
1139 ));
1140 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1144 .iter()
1145 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1146 .collect();
1147 let element_material_colors = Arc::new(element_material_colors);
1148
1149 let total_jobs = entity_jobs.len();
1150 let initial_chunk_size = options.initial_batch_size.max(1);
1151 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1152 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1153 FxHashMap::default();
1154 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1155 FxHashMap::default();
1156 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1157 let mut meshes: Vec<MeshData> = Vec::new();
1158 let mut processed_jobs = 0usize;
1159 let mut total_meshes = 0usize;
1160 let mut total_vertices = 0usize;
1161 let mut total_triangles = 0usize;
1162 let mut chunk_start = 0usize;
1163 let mut current_chunk_size = initial_chunk_size;
1164
1165 let mut deferred_styles_applied = !defer_style_updates;
1166
1167 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1170
1171 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1176 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1177
1178 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1182
1183 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1203 && options.retain_emitted_meshes
1204 && coord_space != SITE_LOCAL_MESH_COORDINATE_SPACE)
1205 .then(|| {
1206 Arc::new(
1207 mapped_item_plan
1208 .into_iter()
1209 .filter(|(_, (count, _))| *count >= 2)
1210 .collect::<FxHashMap<u32, (u32, u32)>>(),
1211 )
1212 });
1213 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1232 && !indexed_colour_full.is_empty())
1233 .then(|| {
1234 let ids: FxHashSet<u32> = indexed_colour_full
1235 .iter()
1236 .filter(|(_, m)| m.has_multiple_colours())
1237 .map(|(&id, _)| id)
1238 .collect();
1239 (!ids.is_empty()).then(|| Arc::new(ids))
1240 })
1241 .flatten();
1242 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1245 std::sync::Mutex::new(Vec::new());
1246
1247 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1255 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1256 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1257
1258 let worker_point_caches = jobs::new_worker_point_caches();
1259 let worker_placement_caches = jobs::new_worker_placement_caches();
1263
1264 let geometry_span = tracing::info_span!(
1265 "geometry",
1266 element_count = total_jobs,
1267 mesh_count = tracing::field::Empty,
1268 triangle_count = tracing::field::Empty,
1269 backstop_count = tracing::field::Empty,
1270 total_csg_failures = tracing::field::Empty,
1271 phase_ms = tracing::field::Empty,
1272 );
1273 let geometry_guard = geometry_span.clone().entered();
1274
1275 while chunk_start < total_jobs {
1276 if options
1280 .cancel
1281 .as_ref()
1282 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1283 {
1284 break;
1285 }
1286 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1287 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1288
1289 #[cfg(not(target_arch = "wasm32"))]
1293 {
1294 let entity_index_for_meta = entity_index_arc.clone();
1296 jobs_chunk.par_iter_mut().for_each(|job| {
1297 if job.global_id.is_some()
1298 || job.name.is_some()
1299 || job.product_definition_shape_id.is_some()
1300 {
1301 return;
1302 }
1303 let local_decoder = entity_index_for_meta.decoder(content);
1304 let Ok(entity) = local_decoder.decode_at_uncached(job.start, job.end) else {
1306 return;
1307 };
1308 job.global_id = normalize_optional_string(entity.get_string(0));
1309 job.name = normalize_optional_string(entity.get_string(2));
1310 job.product_definition_shape_id = entity.get_ref(6);
1311 });
1312
1313 for job in jobs_chunk.iter_mut() {
1315 let Some(pds_id) = job.product_definition_shape_id else {
1316 continue;
1317 };
1318 let resolved_color = color_cache_by_product_definition_shape
1319 .entry(pds_id)
1320 .or_insert_with(|| {
1321 resolve_element_color_for_product_definition_shape(
1322 pds_id,
1323 &geometry_style_index,
1324 &mut decoder,
1325 )
1326 });
1327 if let Some(color) = resolved_color {
1328 job.element_color = *color;
1329 } else if let Some(color) = element_material_color.get(&job.id) {
1330 job.element_color = *color;
1333 }
1334 if options.include_presentation_layers {
1335 let resolved_layer = layer_cache_by_product_definition_shape
1336 .entry(pds_id)
1337 .or_insert_with(|| {
1338 resolve_presentation_layer_for_product_definition_shape(
1339 pds_id,
1340 &presentation_layer_by_assigned_id,
1341 &mut layer_cache_by_representation,
1342 &mut decoder,
1343 )
1344 });
1345 job.presentation_layer = resolved_layer.clone();
1346 }
1347 }
1348 }
1349
1350 #[cfg(target_arch = "wasm32")]
1352 for job in jobs_chunk.iter_mut() {
1353 populate_entity_job_metadata(
1354 job,
1355 &geometry_style_index,
1356 &element_material_color,
1357 &presentation_layer_by_assigned_id,
1358 &mut color_cache_by_product_definition_shape,
1359 &mut layer_cache_by_product_definition_shape,
1360 &mut layer_cache_by_representation,
1361 &mut decoder,
1362 options.include_presentation_layers,
1363 );
1364 }
1365 let site_local_rotation: Option<&Vec<f64>> =
1366 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1367 site_transform.as_ref()
1368 } else {
1369 None
1370 };
1371 let chunk_meshes: Vec<MeshData> = jobs_chunk
1376 .par_iter()
1377 .map(|job| {
1378 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1379 let mut fallback_cache = FxHashMap::default();
1389 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1390 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1391 match slot_guard.as_deref_mut() {
1392 Some(cache) => cache,
1393 None => &mut fallback_cache,
1394 };
1395 let mut fallback_placement_cache = FxHashMap::default();
1404 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1405 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1406 match placement_slot_guard.as_deref_mut() {
1407 Some(cache) => cache,
1408 None => &mut fallback_placement_cache,
1409 };
1410 process_entity_job(
1411 job,
1412 content,
1413 &entity_index_arc,
1414 unit_scale,
1415 rtc_offset,
1416 seed_plane_angle_to_radians,
1417 options.tessellation_quality,
1418 void_index_arc.as_ref(),
1419 skipped_entity_ids.as_ref(),
1420 geometry_style_index.as_ref(),
1421 indexed_colour_full.as_ref(),
1422 element_material_colors.as_ref(),
1423 texture_index.as_ref(),
1424 site_local_rotation,
1425 &diag_collectors,
1426 &item_dedup_cache,
1427 &brep_signature_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 entity_index_arc.decoder(content);
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 open_topology_accepted = *by_reason.get("KernelError").unwrap_or(&0);
1523 let dropped = total_csg_failures - open_topology_accepted;
1524 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1525 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1526 let breakdown = breakdown
1527 .iter()
1528 .map(|(reason, count)| format!("{reason}={count}"))
1529 .collect::<Vec<_>>()
1530 .join(" ");
1531 tracing::warn!(
1532 total_csg_failures,
1533 products_with_failures,
1534 dropped,
1535 open_topology_accepted,
1536 %breakdown,
1537 "{}",
1538 csg_summary::csg_summary_message(dropped, open_topology_accepted)
1539 );
1540 }
1541
1542 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1543 diagnostics::collate(
1544 diag_collectors.classification,
1545 diag_collectors.host_diags,
1546 diag_collectors.rect_fast,
1547 diag_collectors.unsupported_items,
1548 &csg_failures,
1549 oversized_ref_drops,
1550 )
1551 });
1552
1553 let instances = instancing::finalize_instances(
1558 raw_instance_collector
1559 .into_inner()
1560 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1561 &mut meshes,
1562 &mapped_item_cache,
1563 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1564 );
1565
1566 let total_time = total_start.elapsed();
1567 pipeline_span.record("element_count", total_jobs as u64);
1568 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1569
1570 tracing::info!(
1571 meshes = meshes.len(),
1572 instances = instances.len(),
1573 vertices = total_vertices,
1574 triangles = total_triangles,
1575 backstop_count = backstop_dropped,
1576 geometry_time_ms = geometry_time.as_millis(),
1577 total_time_ms = total_time.as_millis(),
1578 "Geometry processing complete"
1579 );
1580
1581 let extract_georeferencing = || crate::georeferencing::extract_georeferencing_from_candidates(
1582 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1583 );
1584 #[cfg(not(target_arch = "wasm32"))]
1588 let georeferencing = rayon::in_place_scope(|scope| {
1589 scope.spawn(move |_| drop(decoder));
1590 scope.spawn(move |_| drop(item_dedup_cache));
1591 extract_georeferencing()
1592 });
1593 #[cfg(target_arch = "wasm32")]
1594 let georeferencing = extract_georeferencing();
1595
1596 ProcessingResult {
1597 meshes,
1598 instances,
1599 mesh_coordinate_space: Some(coord_space.to_string()),
1600 site_transform,
1601 building_transform,
1602 metadata: ModelMetadata {
1603 schema_version,
1604 entity_count: total_entities,
1605 geometry_entity_count,
1606 coordinate_info: CoordinateInfo {
1607 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1608 is_geo_referenced: has_rtc_offset,
1609 },
1610 length_unit_scale: Some(unit_scale),
1611 georeferencing,
1612 },
1613 stats: ProcessingStats {
1614 total_meshes,
1615 total_vertices,
1616 total_triangles,
1617 parse_time_ms: parse_time.as_millis() as u64,
1618 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1619 lookup_time_ms: lookup_time.as_millis() as u64,
1620 preprocess_time_ms: preprocess_time.as_millis() as u64,
1621 geometry_time_ms: geometry_time.as_millis() as u64,
1622 total_time_ms: total_time.as_millis() as u64,
1623 from_cache: false,
1624 total_csg_failures: total_csg_failures as u64,
1625 products_with_failures,
1626 degenerate_triangles_dropped: backstop_dropped,
1627 point_cache_hits,
1628 point_cache_misses,
1629 faceted_brep_time_ms,
1630 geometry_diagnostics,
1631 },
1632 }
1633}
1634
1635