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, 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;
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 site_local::{
59 translation_is_nonidentity, MODEL_RTC_MESH_COORDINATE_SPACE, RAW_IFC_MESH_COORDINATE_SPACE,
60 SITE_LOCAL_MESH_COORDINATE_SPACE,
61};
62
63#[cfg(not(target_arch = "wasm32"))]
70use std::time::Instant as Clock;
71
72#[cfg(target_arch = "wasm32")]
73#[derive(Clone, Copy)]
74struct Clock;
75
76#[cfg(target_arch = "wasm32")]
77impl Clock {
78 #[inline]
79 fn now() -> Self {
80 Clock
81 }
82 #[inline]
83 fn elapsed(&self) -> std::time::Duration {
84 std::time::Duration::ZERO
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum OpeningFilterMode {
92 #[default]
94 Default = 0,
95 IgnoreAll = 1,
97 IgnoreOpaque = 2,
99}
100
101impl OpeningFilterMode {
102 pub fn cache_key_suffix(&self) -> &'static str {
105 match self {
106 Self::Default => "default",
107 Self::IgnoreAll => "ignore_all",
108 Self::IgnoreOpaque => "ignore_opaque",
109 }
110 }
111}
112
113pub struct ProcessingResult {
115 pub meshes: Vec<MeshData>,
116 pub instances: Vec<InstanceRecord>,
123 pub mesh_coordinate_space: Option<String>,
125 pub site_transform: Option<Vec<f64>>,
127 pub building_transform: Option<Vec<f64>>,
129 pub metadata: ModelMetadata,
130 pub stats: ProcessingStats,
131}
132
133#[derive(Debug, Clone)]
137pub struct StreamingOptions {
138 pub initial_batch_size: usize,
140 pub throughput_batch_size: usize,
142 pub fast_first_batch: bool,
144 pub include_properties: bool,
146 pub include_presentation_layers: bool,
148 pub emit_quick_metadata_bootstrap: bool,
150 pub retain_emitted_meshes: bool,
152 pub tessellation_quality: TessellationQuality,
159 pub entity_index: Option<Arc<EntityIndex>>,
167 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
173 pub enable_instancing: bool,
182}
183
184impl Default for StreamingOptions {
185 fn default() -> Self {
186 Self {
187 initial_batch_size: 50,
188 throughput_batch_size: 50,
189 fast_first_batch: false,
190 include_properties: true,
191 include_presentation_layers: true,
192 emit_quick_metadata_bootstrap: false,
193 retain_emitted_meshes: true,
194 tessellation_quality: TessellationQuality::default(),
195 entity_index: None,
196 cancel: None,
197 enable_instancing: false,
198 }
199 }
200}
201
202pub(super) struct EntityJob {
204 pub(super) id: u32,
205 pub(super) ifc_type: IfcType,
206 pub(super) start: usize,
207 pub(super) end: usize,
208 pub(super) product_definition_shape_id: Option<u32>,
209 pub(super) element_color: [f32; 4],
210 pub(super) global_id: Option<String>,
211 pub(super) name: Option<String>,
212 pub(super) presentation_layer: Option<String>,
213 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
214 pub(super) representation_map_id: Option<u32>,
218}
219
220#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
222#[allow(clippy::too_many_arguments)]
224fn populate_entity_job_metadata(
225 job: &mut EntityJob,
226 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
227 element_material_color: &FxHashMap<u32, [f32; 4]>,
228 layer_by_assigned_representation: &FxHashMap<u32, String>,
229 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
230 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
231 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
232 decoder: &mut EntityDecoder,
233 include_presentation_layers: bool,
234) {
235 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
236 return;
237 }
238
239 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
240 return;
241 };
242
243 job.global_id = normalize_optional_string(entity.get_string(0));
244 job.name = normalize_optional_string(entity.get_string(2));
245 job.product_definition_shape_id = entity.get_ref(6);
246
247 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
248 return;
249 };
250
251 let resolved_color = color_cache_by_product_definition_shape
252 .entry(product_definition_shape_id)
253 .or_insert_with(|| {
254 resolve_element_color_for_product_definition_shape(
255 product_definition_shape_id,
256 geometry_style_index,
257 decoder,
258 )
259 });
260 if let Some(color) = resolved_color {
261 job.element_color = *color;
262 } else if let Some(color) = element_material_color.get(&job.id) {
263 job.element_color = *color;
264 }
265
266 if include_presentation_layers {
267 let resolved_layer = layer_cache_by_product_definition_shape
268 .entry(product_definition_shape_id)
269 .or_insert_with(|| {
270 resolve_presentation_layer_for_product_definition_shape(
271 product_definition_shape_id,
272 layer_by_assigned_representation,
273 layer_cache_by_representation,
274 decoder,
275 )
276 });
277 job.presentation_layer = resolved_layer.clone();
278 }
279}
280
281use crate::style::GeometryStyleInfo;
285
286pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
288 let list = entity.get_list(index)?;
289 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
290 if refs.is_empty() {
291 None
292 } else {
293 Some(refs)
294 }
295}
296
297pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
298 let value = raw?.trim();
299 if value.is_empty() || value == "$" {
300 return None;
301 }
302 Some(value.to_string())
303}
304
305fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
306 match ifc_type {
307 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
308 IfcType::IfcSlab => 95,
309 IfcType::IfcColumn => 90,
310 IfcType::IfcBeam => 85,
311 IfcType::IfcRoof => 80,
312 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
313 IfcType::IfcCurtainWall => 70,
314 IfcType::IfcFooting | IfcType::IfcPile => 65,
315 IfcType::IfcDoor | IfcType::IfcWindow => 30,
316 IfcType::IfcFurnishingElement => 10,
317 _ => 50,
318 }
319}
320
321pub fn process_geometry<T>(content: &T) -> ProcessingResult
323where
324 T: AsRef<[u8]> + ?Sized,
325{
326 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
327}
328
329pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
336where
337 T: AsRef<[u8]> + ?Sized,
338{
339 process_geometry_streaming_filtered_with_options(
340 content.as_ref(),
341 OpeningFilterMode::Default,
342 StreamingOptions {
343 initial_batch_size: usize::MAX,
344 throughput_batch_size: usize::MAX,
345 entity_index: Some(index),
346 ..StreamingOptions::default()
347 },
348 |_, _, _| {},
349 |_| {},
350 |_| {},
351 )
352}
353
354pub fn process_geometry_streaming(
356 content: &[u8],
357 batch_size: usize,
358 on_batch: impl FnMut(&[MeshData], usize, usize),
359) -> ProcessingResult {
360 process_geometry_streaming_with_options(
361 content,
362 StreamingOptions {
363 initial_batch_size: batch_size,
364 throughput_batch_size: batch_size,
365 ..StreamingOptions::default()
366 },
367 on_batch,
368 |_| {},
369 )
370}
371
372pub fn process_geometry_streaming_with_options(
374 content: &[u8],
375 options: StreamingOptions,
376 on_batch: impl FnMut(&[MeshData], usize, usize),
377 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
378) -> ProcessingResult {
379 process_geometry_streaming_with_options_and_bootstrap(
380 content,
381 options,
382 on_batch,
383 on_color_update,
384 |_| {},
385 )
386}
387
388pub fn process_geometry_streaming_with_options_and_bootstrap(
391 content: &[u8],
392 options: StreamingOptions,
393 on_batch: impl FnMut(&[MeshData], usize, usize),
394 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
395 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
396) -> ProcessingResult {
397 process_geometry_streaming_filtered_with_options(
398 content,
399 OpeningFilterMode::Default,
400 options,
401 on_batch,
402 on_color_update,
403 on_quick_metadata_bootstrap,
404 )
405}
406
407pub fn process_geometry_filtered<T>(
409 content: &T,
410 opening_filter: OpeningFilterMode,
411) -> ProcessingResult
412where
413 T: AsRef<[u8]> + ?Sized,
414{
415 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
416}
417
418pub fn process_geometry_filtered_with_quality<T>(
422 content: &T,
423 opening_filter: OpeningFilterMode,
424 tessellation_quality: TessellationQuality,
425) -> ProcessingResult
426where
427 T: AsRef<[u8]> + ?Sized,
428{
429 let content = content.as_ref();
430 process_geometry_streaming_filtered_with_options(
431 content,
432 opening_filter,
433 StreamingOptions {
434 initial_batch_size: usize::MAX,
435 throughput_batch_size: usize::MAX,
436 tessellation_quality,
437 ..StreamingOptions::default()
438 },
439 |_, _, _| {},
440 |_| {},
441 |_| {},
442 )
443}
444
445pub fn process_geometry_streaming_filtered(
447 content: &[u8],
448 opening_filter: OpeningFilterMode,
449 batch_size: usize,
450 on_batch: impl FnMut(&[MeshData], usize, usize),
451 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
452) -> ProcessingResult {
453 process_geometry_streaming_filtered_with_options(
454 content,
455 opening_filter,
456 StreamingOptions {
457 initial_batch_size: batch_size,
458 throughput_batch_size: batch_size,
459 ..StreamingOptions::default()
460 },
461 on_batch,
462 on_color_update,
463 |_| {},
464 )
465}
466
467pub fn process_geometry_streaming_filtered_with_options(
469 content: &[u8],
470 opening_filter: OpeningFilterMode,
471 options: StreamingOptions,
472 mut on_batch: impl FnMut(&[MeshData], usize, usize),
473 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
474 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
475) -> ProcessingResult {
476 let total_start = Clock::now();
477 let parse_start = Clock::now();
478 let entity_scan_start = Clock::now();
479
480 let pipeline_span = tracing::info_span!(
487 "geometry_pipeline",
488 byte_size = content.len(),
489 element_count = tracing::field::Empty,
490 total_ms = tracing::field::Empty,
491 );
492 let _pipeline_guard = pipeline_span.clone().entered();
493
494 tracing::info!(
495 content_size = content.len(),
496 "Starting IFC geometry processing"
497 );
498
499 let scan_span = tracing::info_span!(
500 "scan_prepass",
501 total_entities = tracing::field::Empty,
502 geometry_entities = tracing::field::Empty,
503 phase_ms = tracing::field::Empty,
504 );
505 let scan_guard = scan_span.clone().entered();
506
507 let provided_index = options.entity_index.clone();
514 let building_index = provided_index.is_none();
515 let mut inline_index = IndexBuilder::new(content.len(), building_index);
516 let mut decoder = match &provided_index {
517 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
518 None => EntityDecoder::new(content),
519 };
520 tracing::debug!("Entity index will be built inline during the scan");
521
522 let mut prepass_spans = crate::prepass::PrepassSpans::default();
528 let mut project_id: Option<u32> = None;
529 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
530 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
537
538 let mut scanner = EntityScanner::new(content);
540 let mut georeferencing_candidates = Vec::new();
541 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
542 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
547 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
548 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
553 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
562 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
563 let mut quick_spatial_nodes =
564 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
565 let mut quick_aggregate_links = if quick_metadata_enabled {
566 Vec::<(u32, Vec<u32>)>::new()
567 } else {
568 Vec::new()
569 };
570 let mut quick_containment_links = if quick_metadata_enabled {
571 Vec::<(u32, Vec<u32>)>::new()
572 } else {
573 Vec::new()
574 };
575 let mut quick_referenced_links = if quick_metadata_enabled {
580 Vec::<(u32, Vec<u32>)>::new()
581 } else {
582 Vec::new()
583 };
584 let mut quick_element_summaries = if quick_metadata_enabled {
585 HashMap::<u32, QuickMetadataEntitySummary>::new()
586 } else {
587 HashMap::new()
588 };
589 let mut total_entities = 0usize;
590 let mut site_entity_pos: Option<(usize, usize)> = None;
591 let mut building_entity_pos: Option<(usize, usize)> = None;
592
593 let defer_style_updates = options.fast_first_batch
594 && opening_filter == OpeningFilterMode::Default
595 && !options.include_presentation_layers;
596
597 while let Some((id, type_name, start, end)) = scanner.next_entity() {
598 total_entities += 1;
599 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
600 georeferencing_candidates.push((id, ifc_type));
601 }
602 if building_index {
603 inline_index.insert(id, (start, end));
604 }
605 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
606 if is_quick_spatial_type_ci(type_name) {
608 let args = parse_step_arguments(&content[start..end]);
609 let fallback = format!("{type_name} #{id}");
610 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
611 express_id: id,
612 type_name: type_name.to_string(),
613 name: extract_name_from_args(&args, &fallback),
614 elevation: if keyword_eq(type_name, "IFCBUILDINGSTOREY") {
615 extract_storey_elevation_from_args(&args)
616 } else {
617 None
618 },
619 children: Vec::new(),
620 elements: Vec::new(),
621 parent: None,
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 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
918 &entity_jobs,
919 &void_index,
920 &filling_by_opening,
921 &geometry_style_index,
922 &mut decoder,
923 opening_filter,
924 );
925
926 let schema_version = schema_detection::detect_schema_version(content).to_string();
927
928 let geometry_entity_count = entity_jobs.len();
929 tracing::info!(
930 total_entities = total_entities,
931 geometry_entities = geometry_entity_count,
932 voids = void_index.len(),
933 schema_version = %schema_version,
934 "Entity scanning complete"
935 );
936
937 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
938 for (parent_id, child_ids) in quick_aggregate_links {
939 if !spatial_nodes.contains_key(&parent_id) {
940 continue;
941 }
942 for child_id in child_ids {
943 if !spatial_nodes.contains_key(&child_id) {
944 continue;
945 }
946 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
947 parent.children.push(child_id);
948 }
949 if let Some(child) = spatial_nodes.get_mut(&child_id) {
950 child.parent = Some(parent_id);
951 }
952 }
953 }
954 for (parent_id, element_ids) in quick_containment_links {
955 if !spatial_nodes.contains_key(&parent_id) {
956 continue;
957 }
958 for child_id in element_ids {
959 if spatial_nodes.contains_key(&child_id) {
966 let already_placed = spatial_nodes
969 .get(&child_id)
970 .is_some_and(|child| child.parent.is_some());
971 if !already_placed {
972 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
973 parent.children.push(child_id);
974 }
975 if let Some(child) = spatial_nodes.get_mut(&child_id) {
976 child.parent = Some(parent_id);
977 }
978 }
979 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
980 parent.elements.push(child_id);
981 }
982 }
983 }
984 for (parent_id, element_ids) in quick_referenced_links {
988 if !spatial_nodes.contains_key(&parent_id) {
989 continue;
990 }
991 for child_id in element_ids {
992 if spatial_nodes.contains_key(&child_id) {
995 continue;
996 }
997 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
998 parent.elements.push(child_id);
999 }
1000 }
1001 }
1002 let root_id = spatial_nodes
1006 .values()
1007 .filter(|node| keyword_eq(&node.type_name, "IFCPROJECT"))
1008 .map(|node| node.express_id)
1009 .min()
1010 .or_else(|| {
1011 spatial_nodes
1012 .values()
1013 .filter(|node| node.parent.is_none())
1014 .map(|node| node.express_id)
1015 .min()
1016 });
1017 let spatial_tree = root_id
1018 .map(|root| {
1019 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1020 })
1021 .transpose()
1022 .unwrap_or(None);
1023 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1024 schema_version: schema_version.clone(),
1025 entity_count: total_entities,
1026 spatial_tree,
1027 });
1028 }
1029
1030 let preprocess_start = Clock::now();
1032 let preprocess_span =
1033 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1034 let unit_scales = tracing::debug_span!("unit_scale")
1040 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1041 tracing::debug!(
1042 length_unit_scale = unit_scales.length_unit_scale,
1043 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1044 "Resolved unit scales"
1045 );
1046 decoder.seed_unit_scales(
1047 unit_scales.length_unit_scale,
1048 unit_scales.plane_angle_to_radians,
1049 );
1050 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1052 router.set_tessellation_quality(options.tessellation_quality);
1053 let material_spans = &prepass_spans.rel_associates_material;
1057 router.set_material_layer_index(Arc::new(
1058 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1059 ));
1060
1061 let site_transform: Option<Vec<f64>> = site_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 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1070 let entity = decoder.decode_at(start, end).ok()?;
1071 let matrix = router
1072 .resolve_scaled_placement(&entity, &mut decoder)
1073 .ok()?;
1074 Some(matrix.to_vec())
1075 });
1076
1077 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1078 .iter()
1079 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1080 .collect();
1081 let detected_rtc_offset =
1082 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1083
1084 let site_rtc = site_transform
1093 .as_ref()
1094 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1096 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1097 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1098 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1099 } else if detected_has_offset {
1100 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1101 } else {
1102 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1103 };
1104 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1105 router.set_rtc_offset(rtc_offset);
1106 let preprocess_time = preprocess_start.elapsed();
1107 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1108 drop(preprocess_span);
1109
1110 let parse_time = parse_start.elapsed();
1111 tracing::info!(
1112 entity_scan_time_ms = entity_scan_time.as_millis(),
1113 lookup_time_ms = lookup_time.as_millis(),
1114 preprocess_time_ms = preprocess_time.as_millis(),
1115 parse_time_ms = parse_time.as_millis(),
1116 "Parse phase complete, starting geometry extraction"
1117 );
1118
1119 let geometry_start = Clock::now();
1121 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1123 let rtc_offset = router.rtc_offset();
1124 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1132 let void_index_arc = Arc::new(filtered_void_index);
1133 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1134 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1137 let mut geometry_style_index = Arc::new(geometry_style_index);
1138 let indexed_colour_full = Arc::new(indexed_colour_full);
1139 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1144 content,
1145 &mut decoder,
1146 ));
1147 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1151 .iter()
1152 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1153 .collect();
1154 let element_material_colors = Arc::new(element_material_colors);
1155
1156 let total_jobs = entity_jobs.len();
1157 let initial_chunk_size = options.initial_batch_size.max(1);
1158 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1159 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1160 FxHashMap::default();
1161 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1162 FxHashMap::default();
1163 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1164 let mut meshes: Vec<MeshData> = Vec::new();
1165 let mut processed_jobs = 0usize;
1166 let mut total_meshes = 0usize;
1167 let mut total_vertices = 0usize;
1168 let mut total_triangles = 0usize;
1169 let mut chunk_start = 0usize;
1170 let mut current_chunk_size = initial_chunk_size;
1171
1172 let mut deferred_styles_applied = !defer_style_updates;
1173
1174 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1177
1178 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1183 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1184
1185 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1189
1190 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1210 && options.retain_emitted_meshes
1211 && coord_space != SITE_LOCAL_MESH_COORDINATE_SPACE)
1212 .then(|| {
1213 Arc::new(
1214 mapped_item_plan
1215 .into_iter()
1216 .filter(|(_, (count, _))| *count >= 2)
1217 .collect::<FxHashMap<u32, (u32, u32)>>(),
1218 )
1219 });
1220 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1239 && !indexed_colour_full.is_empty())
1240 .then(|| {
1241 let ids: FxHashSet<u32> = indexed_colour_full
1242 .iter()
1243 .filter(|(_, m)| m.has_multiple_colours())
1244 .map(|(&id, _)| id)
1245 .collect();
1246 (!ids.is_empty()).then(|| Arc::new(ids))
1247 })
1248 .flatten();
1249 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1252 std::sync::Mutex::new(Vec::new());
1253
1254 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1262 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1263 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1264
1265 let worker_point_caches = jobs::new_worker_point_caches();
1266 let worker_placement_caches = jobs::new_worker_placement_caches();
1270
1271 let geometry_span = tracing::info_span!(
1272 "geometry",
1273 element_count = total_jobs,
1274 mesh_count = tracing::field::Empty,
1275 triangle_count = tracing::field::Empty,
1276 backstop_count = tracing::field::Empty,
1277 total_csg_failures = tracing::field::Empty,
1278 phase_ms = tracing::field::Empty,
1279 );
1280 let geometry_guard = geometry_span.clone().entered();
1281
1282 while chunk_start < total_jobs {
1283 if options
1287 .cancel
1288 .as_ref()
1289 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1290 {
1291 break;
1292 }
1293 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1294 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1295
1296 #[cfg(not(target_arch = "wasm32"))]
1300 {
1301 let entity_index_for_meta = entity_index_arc.clone();
1303 jobs_chunk.par_iter_mut().for_each(|job| {
1304 if job.global_id.is_some()
1305 || job.name.is_some()
1306 || job.product_definition_shape_id.is_some()
1307 {
1308 return;
1309 }
1310 let local_decoder = entity_index_for_meta.decoder(content);
1311 let Ok(entity) = local_decoder.decode_at_uncached(job.start, job.end) else {
1313 return;
1314 };
1315 job.global_id = normalize_optional_string(entity.get_string(0));
1316 job.name = normalize_optional_string(entity.get_string(2));
1317 job.product_definition_shape_id = entity.get_ref(6);
1318 });
1319
1320 for job in jobs_chunk.iter_mut() {
1322 let Some(pds_id) = job.product_definition_shape_id else {
1323 continue;
1324 };
1325 let resolved_color = color_cache_by_product_definition_shape
1326 .entry(pds_id)
1327 .or_insert_with(|| {
1328 resolve_element_color_for_product_definition_shape(
1329 pds_id,
1330 &geometry_style_index,
1331 &mut decoder,
1332 )
1333 });
1334 if let Some(color) = resolved_color {
1335 job.element_color = *color;
1336 } else if let Some(color) = element_material_color.get(&job.id) {
1337 job.element_color = *color;
1340 }
1341 if options.include_presentation_layers {
1342 let resolved_layer = layer_cache_by_product_definition_shape
1343 .entry(pds_id)
1344 .or_insert_with(|| {
1345 resolve_presentation_layer_for_product_definition_shape(
1346 pds_id,
1347 &presentation_layer_by_assigned_id,
1348 &mut layer_cache_by_representation,
1349 &mut decoder,
1350 )
1351 });
1352 job.presentation_layer = resolved_layer.clone();
1353 }
1354 }
1355 }
1356
1357 #[cfg(target_arch = "wasm32")]
1359 for job in jobs_chunk.iter_mut() {
1360 populate_entity_job_metadata(
1361 job,
1362 &geometry_style_index,
1363 &element_material_color,
1364 &presentation_layer_by_assigned_id,
1365 &mut color_cache_by_product_definition_shape,
1366 &mut layer_cache_by_product_definition_shape,
1367 &mut layer_cache_by_representation,
1368 &mut decoder,
1369 options.include_presentation_layers,
1370 );
1371 }
1372 let site_local_rotation: Option<&Vec<f64>> =
1373 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1374 site_transform.as_ref()
1375 } else {
1376 None
1377 };
1378 let chunk_meshes: Vec<MeshData> = jobs_chunk
1383 .par_iter()
1384 .map(|job| {
1385 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1386 let mut fallback_cache = FxHashMap::default();
1396 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1397 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1398 match slot_guard.as_deref_mut() {
1399 Some(cache) => cache,
1400 None => &mut fallback_cache,
1401 };
1402 let mut fallback_placement_cache = FxHashMap::default();
1411 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1412 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1413 match placement_slot_guard.as_deref_mut() {
1414 Some(cache) => cache,
1415 None => &mut fallback_placement_cache,
1416 };
1417 process_entity_job(
1418 job,
1419 content,
1420 &entity_index_arc,
1421 unit_scale,
1422 rtc_offset,
1423 seed_plane_angle_to_radians,
1424 options.tessellation_quality,
1425 void_index_arc.as_ref(),
1426 skipped_entity_ids.as_ref(),
1427 geometry_style_index.as_ref(),
1428 indexed_colour_full.as_ref(),
1429 element_material_colors.as_ref(),
1430 texture_index.as_ref(),
1431 site_local_rotation,
1432 &diag_collectors,
1433 &item_dedup_cache,
1434 &brep_signature_cache,
1435 &mapped_item_cache,
1436 instancing_plan.as_ref(),
1437 indexed_colour_split_ids.as_ref(),
1438 &raw_instance_collector,
1439 worker_point_cache,
1440 worker_placement_cache,
1441 &point_cache_hits_collector,
1442 &point_cache_misses_collector,
1443 &faceted_brep_ns_collector,
1444 )
1445 })
1446 .flatten_iter()
1447 .collect();
1448
1449 processed_jobs += jobs_chunk.len();
1450 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1451 total_triangles += chunk_meshes
1452 .iter()
1453 .map(|m| m.triangle_count())
1454 .sum::<usize>();
1455
1456 if !chunk_meshes.is_empty() {
1457 total_meshes += chunk_meshes.len();
1458 let emit_mesh_chunk_size = current_chunk_size.max(1);
1459 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1460 on_batch(emitted_meshes, processed_jobs, total_jobs);
1461 }
1462 if options.retain_emitted_meshes {
1463 meshes.extend(chunk_meshes);
1464 }
1465
1466 if !deferred_styles_applied {
1467 let mut rebuilt_styles = {
1472 let mut style_decoder =
1473 entity_index_arc.decoder(content);
1474 crate::prepass::resolve_styled_item_spans(
1475 &deferred_styled_item_positions,
1476 &mut style_decoder,
1477 )
1478 };
1479 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1480 geometry_style_index = Arc::new(rebuilt_styles);
1481 let deferred_color_updates = build_color_updates_for_jobs(
1482 &entity_jobs[..processed_jobs],
1483 geometry_style_index.as_ref(),
1484 content,
1485 &entity_index_arc,
1486 );
1487 if !deferred_color_updates.is_empty() {
1488 on_color_update(&deferred_color_updates);
1489 }
1490 deferred_styles_applied = true;
1491 }
1492 }
1493 chunk_start = chunk_end;
1494 current_chunk_size = throughput_chunk_size;
1495 }
1496
1497 let geometry_time = geometry_start.elapsed();
1498 let csg_failures = diag_collectors
1501 .csg_failures
1502 .into_inner()
1503 .unwrap_or_else(|poisoned| poisoned.into_inner());
1504 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1505 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1506 let backstop_dropped = diag_collectors.backstop.into_inner();
1507 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1509 let point_cache_hits = point_cache_hits_collector.into_inner();
1510 let point_cache_misses = point_cache_misses_collector.into_inner();
1511 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1512 geometry_span.record("mesh_count", total_meshes as u64);
1513 geometry_span.record("triangle_count", total_triangles as u64);
1514 geometry_span.record("backstop_count", backstop_dropped);
1515 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1516 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1517 drop(geometry_guard);
1518 if total_csg_failures > 0 {
1519 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1520 for fails in csg_failures.values() {
1521 for f in fails {
1522 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1523 }
1524 }
1525 let open_topology_accepted = *by_reason.get("KernelError").unwrap_or(&0);
1530 let dropped = total_csg_failures - open_topology_accepted;
1531 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1532 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1533 let breakdown = breakdown
1534 .iter()
1535 .map(|(reason, count)| format!("{reason}={count}"))
1536 .collect::<Vec<_>>()
1537 .join(" ");
1538 tracing::warn!(
1539 total_csg_failures,
1540 products_with_failures,
1541 dropped,
1542 open_topology_accepted,
1543 %breakdown,
1544 "{}",
1545 csg_summary::csg_summary_message(dropped, open_topology_accepted)
1546 );
1547 }
1548
1549 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1550 diagnostics::collate(
1551 diag_collectors.classification,
1552 diag_collectors.host_diags,
1553 diag_collectors.rect_fast,
1554 diag_collectors.unsupported_items,
1555 &csg_failures,
1556 oversized_ref_drops,
1557 )
1558 });
1559
1560 let instances = instancing::finalize_instances(
1565 raw_instance_collector
1566 .into_inner()
1567 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1568 &mut meshes,
1569 &mapped_item_cache,
1570 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1571 );
1572
1573 let total_time = total_start.elapsed();
1574 pipeline_span.record("element_count", total_jobs as u64);
1575 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1576
1577 tracing::info!(
1578 meshes = meshes.len(),
1579 instances = instances.len(),
1580 vertices = total_vertices,
1581 triangles = total_triangles,
1582 backstop_count = backstop_dropped,
1583 geometry_time_ms = geometry_time.as_millis(),
1584 total_time_ms = total_time.as_millis(),
1585 "Geometry processing complete"
1586 );
1587
1588 let extract_georeferencing = || crate::georeferencing::extract_georeferencing_from_candidates(
1589 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1590 );
1591 #[cfg(not(target_arch = "wasm32"))]
1595 let georeferencing = rayon::in_place_scope(|scope| {
1596 scope.spawn(move |_| drop(decoder));
1597 scope.spawn(move |_| drop(item_dedup_cache));
1598 extract_georeferencing()
1599 });
1600 #[cfg(target_arch = "wasm32")]
1601 let georeferencing = extract_georeferencing();
1602
1603 ProcessingResult {
1604 meshes,
1605 instances,
1606 mesh_coordinate_space: Some(coord_space.to_string()),
1607 site_transform,
1608 building_transform,
1609 metadata: ModelMetadata {
1610 schema_version,
1611 entity_count: total_entities,
1612 geometry_entity_count,
1613 coordinate_info: CoordinateInfo {
1614 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1615 is_geo_referenced: has_rtc_offset,
1616 },
1617 length_unit_scale: Some(unit_scale),
1618 georeferencing,
1619 },
1620 stats: ProcessingStats {
1621 total_meshes,
1622 total_vertices,
1623 total_triangles,
1624 parse_time_ms: parse_time.as_millis() as u64,
1625 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1626 lookup_time_ms: lookup_time.as_millis() as u64,
1627 preprocess_time_ms: preprocess_time.as_millis() as u64,
1628 geometry_time_ms: geometry_time.as_millis() as u64,
1629 total_time_ms: total_time.as_millis() as u64,
1630 from_cache: false,
1631 total_csg_failures: total_csg_failures as u64,
1632 products_with_failures,
1633 degenerate_triangles_dropped: backstop_dropped,
1634 point_cache_hits,
1635 point_cache_misses,
1636 faceted_brep_time_ms,
1637 geometry_diagnostics,
1638 },
1639 }
1640}
1641
1642