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 instancing;
27mod jobs;
28mod opening_filter;
29mod properties;
30mod quick_metadata;
31mod site_local;
32
33pub use site_local::convert_mesh_to_site_local;
34
35use jobs::{build_color_updates_for_jobs, process_entity_job};
36
37use color_layer::{
38 collect_presentation_layer_assignments, resolve_element_color_for_product_definition_shape,
39 resolve_presentation_layer_for_product_definition_shape,
40};
41use opening_filter::apply_opening_filter;
42use properties::resolve_space_zone_properties_lazy;
43use quick_metadata::{
44 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
45 is_quick_spatial_type_ci, parse_step_arguments, parse_step_ref, parse_step_ref_list,
46 QuickSpatialNodeEntry,
47};
48use site_local::{
49 translation_is_nonidentity, MODEL_RTC_MESH_COORDINATE_SPACE, RAW_IFC_MESH_COORDINATE_SPACE,
50 SITE_LOCAL_MESH_COORDINATE_SPACE,
51};
52
53#[cfg(not(target_arch = "wasm32"))]
60use std::time::Instant as Clock;
61
62#[cfg(target_arch = "wasm32")]
63#[derive(Clone, Copy)]
64struct Clock;
65
66#[cfg(target_arch = "wasm32")]
67impl Clock {
68 #[inline]
69 fn now() -> Self {
70 Clock
71 }
72 #[inline]
73 fn elapsed(&self) -> std::time::Duration {
74 std::time::Duration::ZERO
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum OpeningFilterMode {
82 #[default]
84 Default = 0,
85 IgnoreAll = 1,
87 IgnoreOpaque = 2,
89}
90
91impl OpeningFilterMode {
92 pub fn cache_key_suffix(&self) -> &'static str {
95 match self {
96 Self::Default => "default",
97 Self::IgnoreAll => "ignore_all",
98 Self::IgnoreOpaque => "ignore_opaque",
99 }
100 }
101}
102
103pub struct ProcessingResult {
105 pub meshes: Vec<MeshData>,
106 pub instances: Vec<InstanceRecord>,
113 pub mesh_coordinate_space: Option<String>,
115 pub site_transform: Option<Vec<f64>>,
117 pub building_transform: Option<Vec<f64>>,
119 pub metadata: ModelMetadata,
120 pub stats: ProcessingStats,
121}
122
123#[derive(Debug, Clone)]
127pub struct StreamingOptions {
128 pub initial_batch_size: usize,
130 pub throughput_batch_size: usize,
132 pub fast_first_batch: bool,
134 pub include_properties: bool,
136 pub include_presentation_layers: bool,
138 pub emit_quick_metadata_bootstrap: bool,
140 pub retain_emitted_meshes: bool,
142 pub tessellation_quality: TessellationQuality,
149 pub entity_index: Option<Arc<EntityIndex>>,
157 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
163 pub enable_instancing: bool,
172}
173
174impl Default for StreamingOptions {
175 fn default() -> Self {
176 Self {
177 initial_batch_size: 50,
178 throughput_batch_size: 50,
179 fast_first_batch: false,
180 include_properties: true,
181 include_presentation_layers: true,
182 emit_quick_metadata_bootstrap: false,
183 retain_emitted_meshes: true,
184 tessellation_quality: TessellationQuality::default(),
185 entity_index: None,
186 cancel: None,
187 enable_instancing: false,
188 }
189 }
190}
191
192pub(super) struct EntityJob {
194 pub(super) id: u32,
195 pub(super) ifc_type: IfcType,
196 pub(super) start: usize,
197 pub(super) end: usize,
198 pub(super) product_definition_shape_id: Option<u32>,
199 pub(super) element_color: [f32; 4],
200 pub(super) global_id: Option<String>,
201 pub(super) name: Option<String>,
202 pub(super) presentation_layer: Option<String>,
203 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
204 pub(super) representation_map_id: Option<u32>,
208}
209
210#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
212#[allow(clippy::too_many_arguments)]
214fn populate_entity_job_metadata(
215 job: &mut EntityJob,
216 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
217 element_material_color: &FxHashMap<u32, [f32; 4]>,
218 layer_by_assigned_representation: &FxHashMap<u32, String>,
219 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
220 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
221 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
222 decoder: &mut EntityDecoder,
223 include_presentation_layers: bool,
224) {
225 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
226 return;
227 }
228
229 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
230 return;
231 };
232
233 job.global_id = normalize_optional_string(entity.get_string(0));
234 job.name = normalize_optional_string(entity.get_string(2));
235 job.product_definition_shape_id = entity.get_ref(6);
236
237 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
238 return;
239 };
240
241 let resolved_color = color_cache_by_product_definition_shape
242 .entry(product_definition_shape_id)
243 .or_insert_with(|| {
244 resolve_element_color_for_product_definition_shape(
245 product_definition_shape_id,
246 geometry_style_index,
247 decoder,
248 )
249 });
250 if let Some(color) = resolved_color {
251 job.element_color = *color;
252 } else if let Some(color) = element_material_color.get(&job.id) {
253 job.element_color = *color;
254 }
255
256 if include_presentation_layers {
257 let resolved_layer = layer_cache_by_product_definition_shape
258 .entry(product_definition_shape_id)
259 .or_insert_with(|| {
260 resolve_presentation_layer_for_product_definition_shape(
261 product_definition_shape_id,
262 layer_by_assigned_representation,
263 layer_cache_by_representation,
264 decoder,
265 )
266 });
267 job.presentation_layer = resolved_layer.clone();
268 }
269}
270
271use crate::style::GeometryStyleInfo;
275
276pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
278 let list = entity.get_list(index)?;
279 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
280 if refs.is_empty() {
281 None
282 } else {
283 Some(refs)
284 }
285}
286
287pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
288 let value = raw?.trim();
289 if value.is_empty() || value == "$" {
290 return None;
291 }
292 Some(value.to_string())
293}
294
295fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
296 match ifc_type {
297 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
298 IfcType::IfcSlab => 95,
299 IfcType::IfcColumn => 90,
300 IfcType::IfcBeam => 85,
301 IfcType::IfcRoof => 80,
302 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
303 IfcType::IfcCurtainWall => 70,
304 IfcType::IfcFooting | IfcType::IfcPile => 65,
305 IfcType::IfcDoor | IfcType::IfcWindow => 30,
306 IfcType::IfcFurnishingElement => 10,
307 _ => 50,
308 }
309}
310
311pub fn process_geometry<T>(content: &T) -> ProcessingResult
313where
314 T: AsRef<[u8]> + ?Sized,
315{
316 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
317}
318
319pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
326where
327 T: AsRef<[u8]> + ?Sized,
328{
329 process_geometry_streaming_filtered_with_options(
330 content.as_ref(),
331 OpeningFilterMode::Default,
332 StreamingOptions {
333 initial_batch_size: usize::MAX,
334 throughput_batch_size: usize::MAX,
335 entity_index: Some(index),
336 ..StreamingOptions::default()
337 },
338 |_, _, _| {},
339 |_| {},
340 |_| {},
341 )
342}
343
344pub fn process_geometry_streaming(
346 content: &[u8],
347 batch_size: usize,
348 on_batch: impl FnMut(&[MeshData], usize, usize),
349) -> ProcessingResult {
350 process_geometry_streaming_with_options(
351 content,
352 StreamingOptions {
353 initial_batch_size: batch_size,
354 throughput_batch_size: batch_size,
355 ..StreamingOptions::default()
356 },
357 on_batch,
358 |_| {},
359 )
360}
361
362pub fn process_geometry_streaming_with_options(
364 content: &[u8],
365 options: StreamingOptions,
366 on_batch: impl FnMut(&[MeshData], usize, usize),
367 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
368) -> ProcessingResult {
369 process_geometry_streaming_with_options_and_bootstrap(
370 content,
371 options,
372 on_batch,
373 on_color_update,
374 |_| {},
375 )
376}
377
378pub fn process_geometry_streaming_with_options_and_bootstrap(
381 content: &[u8],
382 options: StreamingOptions,
383 on_batch: impl FnMut(&[MeshData], usize, usize),
384 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
385 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
386) -> ProcessingResult {
387 process_geometry_streaming_filtered_with_options(
388 content,
389 OpeningFilterMode::Default,
390 options,
391 on_batch,
392 on_color_update,
393 on_quick_metadata_bootstrap,
394 )
395}
396
397pub fn process_geometry_filtered<T>(
399 content: &T,
400 opening_filter: OpeningFilterMode,
401) -> ProcessingResult
402where
403 T: AsRef<[u8]> + ?Sized,
404{
405 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
406}
407
408pub fn process_geometry_filtered_with_quality<T>(
412 content: &T,
413 opening_filter: OpeningFilterMode,
414 tessellation_quality: TessellationQuality,
415) -> ProcessingResult
416where
417 T: AsRef<[u8]> + ?Sized,
418{
419 let content = content.as_ref();
420 process_geometry_streaming_filtered_with_options(
421 content,
422 opening_filter,
423 StreamingOptions {
424 initial_batch_size: usize::MAX,
425 throughput_batch_size: usize::MAX,
426 tessellation_quality,
427 ..StreamingOptions::default()
428 },
429 |_, _, _| {},
430 |_| {},
431 |_| {},
432 )
433}
434
435pub fn process_geometry_streaming_filtered(
437 content: &[u8],
438 opening_filter: OpeningFilterMode,
439 batch_size: usize,
440 on_batch: impl FnMut(&[MeshData], usize, usize),
441 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
442) -> ProcessingResult {
443 process_geometry_streaming_filtered_with_options(
444 content,
445 opening_filter,
446 StreamingOptions {
447 initial_batch_size: batch_size,
448 throughput_batch_size: batch_size,
449 ..StreamingOptions::default()
450 },
451 on_batch,
452 on_color_update,
453 |_| {},
454 )
455}
456
457pub fn process_geometry_streaming_filtered_with_options(
459 content: &[u8],
460 opening_filter: OpeningFilterMode,
461 options: StreamingOptions,
462 mut on_batch: impl FnMut(&[MeshData], usize, usize),
463 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
464 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
465) -> ProcessingResult {
466 let total_start = Clock::now();
467 let parse_start = Clock::now();
468 let entity_scan_start = Clock::now();
469
470 let pipeline_span = tracing::info_span!(
477 "geometry_pipeline",
478 byte_size = content.len(),
479 element_count = tracing::field::Empty,
480 total_ms = tracing::field::Empty,
481 );
482 let _pipeline_guard = pipeline_span.clone().entered();
483
484 tracing::info!(
485 content_size = content.len(),
486 "Starting IFC geometry processing"
487 );
488
489 let scan_span = tracing::info_span!(
490 "scan_prepass",
491 total_entities = tracing::field::Empty,
492 geometry_entities = tracing::field::Empty,
493 phase_ms = tracing::field::Empty,
494 );
495 let scan_guard = scan_span.clone().entered();
496
497 let provided_index = options.entity_index.clone();
504 let building_index = provided_index.is_none();
505 let mut inline_index: EntityIndex = if building_index {
506 FxHashMap::with_capacity_and_hasher(content.len() / 50, Default::default())
507 } else {
508 FxHashMap::default()
509 };
510 let mut decoder = match &provided_index {
511 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
512 None => EntityDecoder::new(content),
513 };
514 tracing::debug!("Entity index will be built inline during the scan");
515
516 let mut prepass_spans = crate::prepass::PrepassSpans::default();
522 let mut project_id: Option<u32> = None;
523 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
524 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
531
532 let mut scanner = EntityScanner::new(content);
534 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
535 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
540 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
541 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
546 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
555 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
556 let mut quick_spatial_nodes =
557 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
558 let mut quick_aggregate_links = if quick_metadata_enabled {
559 Vec::<(u32, Vec<u32>)>::new()
560 } else {
561 Vec::new()
562 };
563 let mut quick_containment_links = if quick_metadata_enabled {
564 Vec::<(u32, Vec<u32>)>::new()
565 } else {
566 Vec::new()
567 };
568 let mut quick_referenced_links = if quick_metadata_enabled {
573 Vec::<(u32, Vec<u32>)>::new()
574 } else {
575 Vec::new()
576 };
577 let mut quick_element_summaries = if quick_metadata_enabled {
578 HashMap::<u32, QuickMetadataEntitySummary>::new()
579 } else {
580 HashMap::new()
581 };
582 let mut schema_version = "IFC2X3".to_string();
583 let mut total_entities = 0usize;
584 let mut site_entity_pos: Option<(usize, usize)> = None;
585 let mut building_entity_pos: Option<(usize, usize)> = None;
586
587 let defer_style_updates = options.fast_first_batch
588 && opening_filter == OpeningFilterMode::Default
589 && !options.include_presentation_layers;
590
591 while let Some((id, type_name, start, end)) = scanner.next_entity() {
592 total_entities += 1;
593 if building_index {
594 inline_index.insert(id, (start, end));
595 }
596 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
597 if is_quick_spatial_type_ci(type_name) {
599 let args = parse_step_arguments(&content[start..end]);
600 let fallback = format!("{type_name} #{id}");
601 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
602 express_id: id,
603 type_name: type_name.to_string(),
604 name: extract_name_from_args(&args, &fallback),
605 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
606 extract_storey_elevation_from_args(&args)
607 } else {
608 None
609 },
610 children: Vec::new(),
611 elements: Vec::new(),
612 parent: None,
613 });
614 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
615 let args = parse_step_arguments(&content[start..end]);
616 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
617 quick_aggregate_links.push((
618 parent_id,
619 args.get(5)
620 .map(|token| parse_step_ref_list(token))
621 .unwrap_or_default(),
622 ));
623 }
624 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
625 let args = parse_step_arguments(&content[start..end]);
626 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
627 quick_containment_links.push((
628 parent_id,
629 args.get(4)
630 .map(|token| parse_step_ref_list(token))
631 .unwrap_or_default(),
632 ));
633 }
634 } else if type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
635 let args = parse_step_arguments(&content[start..end]);
636 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
637 quick_referenced_links.push((
638 parent_id,
639 args.get(4)
640 .map(|token| parse_step_ref_list(token))
641 .unwrap_or_default(),
642 ));
643 }
644 }
645 }
646
647 if type_name == "IFCINDEXEDCOLOURMAP" {
648 prepass_spans.indexed_colour_maps.push((id, start, end));
650 continue;
651 }
652
653 if type_name == "IFCSTYLEDITEM" {
654 prepass_spans.styled_items.push((id, start, end));
658 continue;
659 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
660 prepass_spans.material_def_reprs.push((id, start, end));
661 continue;
662 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
663 prepass_spans.rel_associates_material.push((id, start, end));
664 continue;
665 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
666 if !options.include_presentation_layers {
667 continue;
668 }
669 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
670 collect_presentation_layer_assignments(
671 &mut presentation_layer_by_assigned_id,
672 &layer_assignment,
673 );
674 }
675 continue;
676 } else if type_name == "IFCPROPERTYSET" {
677 continue;
680 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
681 if options.include_properties {
682 rel_defines_spans.push((start, end));
683 }
684 continue;
685 } else if type_name.starts_with("IFCPROPERTY") {
686 continue;
689 } else if type_name == "IFCRELVOIDSELEMENT" {
690 prepass_spans.void_rels.push((id, start, end));
691 } else if type_name == "IFCRELFILLSELEMENT" {
692 prepass_spans.fills_rels.push((id, start, end));
693 } else if type_name == "IFCRELAGGREGATES" {
694 prepass_spans.aggregate_rels.push((id, start, end));
699 } else if type_name == "IFCPROJECT" && project_id.is_none() {
700 project_id = Some(id);
701 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
702 site_entity_pos = Some((start, end));
703 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
704 building_entity_pos = Some((start, end));
705 }
706
707 if ifc_lite_core::has_geometry_by_name(type_name) {
708 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
712 if quick_metadata_enabled {
713 quick_element_summaries.insert(
714 id,
715 QuickMetadataEntitySummary {
716 express_id: id,
717 type_name: type_name.to_string(),
718 name: format!("{type_name} #{id}"),
719 global_id: None,
720 kind: "element".to_string(),
721 has_children: false,
722 element_count: None,
723 elevation: None,
724 },
725 );
726 }
727 entity_jobs.push(EntityJob {
728 id,
729 ifc_type,
730 start,
731 end,
732 product_definition_shape_id: None,
733 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
734 global_id: None,
735 name: None,
736 presentation_layer: None,
737 space_zone_properties: None,
738 representation_map_id: None,
739 });
740 } else if ifc_lite_core::is_representationless_spatial_container_by_name(type_name)
741 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
742 {
743 let ifc_type = ifc_lite_core::legacy_aware_ifc_type(type_name);
755 entity_jobs.push(EntityJob {
756 id,
757 ifc_type,
758 start,
759 end,
760 product_definition_shape_id: None,
761 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
762 global_id: None,
763 name: None,
764 presentation_layer: None,
765 space_zone_properties: None,
766 representation_map_id: None,
767 });
768 }
769 else if type_name == "IFCMAPPEDITEM" {
776 let args = parse_step_arguments(&content[start..end]);
777 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
778 referenced_representation_maps.insert(source_id);
779 if options.enable_instancing {
783 mapped_item_plan
784 .entry(source_id)
785 .and_modify(|(count, template)| {
786 *count += 1;
787 if id < *template {
788 *template = id;
789 }
790 })
791 .or_insert((1, id));
792 }
793 }
794 } else if type_name == "IFCRELDEFINESBYTYPE" {
795 let args = parse_step_arguments(&content[start..end]);
798 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
799 instantiated_type_ids.insert(type_id);
800 }
801 } else if (type_name.ends_with("TYPE") || type_name.ends_with("STYLE"))
802 && IfcType::from_str(type_name).is_subtype_of(IfcType::IfcTypeProduct)
803 {
804 let args = parse_step_arguments(&content[start..end]);
805 let rep_map_ids = args
807 .get(6)
808 .map(|token| parse_step_ref_list(token))
809 .unwrap_or_default();
810 if !rep_map_ids.is_empty() {
811 type_product_geometry.push((
812 id,
813 start,
814 end,
815 IfcType::from_str(type_name),
816 rep_map_ids,
817 ));
818 }
819 }
820 }
821
822 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
830 for (rep_map_id, _class) in crate::element::plan_type_geometry(
835 rep_map_ids,
836 &referenced_representation_maps,
837 instantiated_type_ids.contains(type_id),
838 crate::element::TypeGeometryMode::SuppressInstanced,
839 ) {
840 entity_jobs.push(EntityJob {
841 id: *type_id,
842 ifc_type: *ifc_type,
843 start: *start,
844 end: *end,
845 product_definition_shape_id: None,
846 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
847 global_id: None,
848 name: None,
849 presentation_layer: None,
850 space_zone_properties: None,
851 representation_map_id: Some(rep_map_id),
852 });
853 }
854 }
855
856 let entity_index: Arc<EntityIndex> = match provided_index {
861 Some(idx) => idx,
862 None => {
863 let arc = Arc::new(inline_index);
864 decoder.set_entity_index(arc.clone());
865 arc
866 }
867 };
868
869 let resolved = crate::prepass::resolve_prepass(
874 &prepass_spans,
875 &mut decoder,
876 crate::prepass::ResolveOptions {
877 collect_indexed_colour_full: true,
878 defer_attached_styles: defer_style_updates,
879 },
880 );
881 let crate::prepass::ResolvedPrepass {
882 mut geometry_style_index,
883 indexed_colour_index,
884 indexed_colour_full,
885 element_material_colors,
886 void_index,
887 filling_by_opening,
888 deferred_attached_styled_spans: deferred_styled_item_positions,
889 ..
890 } = resolved;
891
892 let entity_scan_time = entity_scan_start.elapsed();
893 scan_span.record("total_entities", total_entities as u64);
894 scan_span.record("geometry_entities", entity_jobs.len() as u64);
895 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
896 drop(scan_guard);
897
898 let lookup_start = Clock::now();
899 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
900 if options.include_properties {
901 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
902 }
903 if options.fast_first_batch {
904 entity_jobs.sort_by(|left, right| {
905 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
906 });
907 }
908 let lookup_time = lookup_start.elapsed();
909 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
910 drop(lookup_span);
911
912 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
913 &entity_jobs,
914 &void_index,
915 &filling_by_opening,
916 &geometry_style_index,
917 &mut decoder,
918 opening_filter,
919 );
920
921 if memchr::memmem::find(content, b"IFC4X3").is_some() {
925 schema_version = "IFC4X3".into();
926 } else if memchr::memmem::find(content, b"IFC4").is_some() {
927 schema_version = "IFC4".into();
928 }
929
930 let geometry_entity_count = entity_jobs.len();
931 tracing::info!(
932 total_entities = total_entities,
933 geometry_entities = geometry_entity_count,
934 voids = void_index.len(),
935 schema_version = %schema_version,
936 "Entity scanning complete"
937 );
938
939 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
940 for (parent_id, child_ids) in quick_aggregate_links {
941 if !spatial_nodes.contains_key(&parent_id) {
942 continue;
943 }
944 for child_id in child_ids {
945 if !spatial_nodes.contains_key(&child_id) {
946 continue;
947 }
948 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
949 parent.children.push(child_id);
950 }
951 if let Some(child) = spatial_nodes.get_mut(&child_id) {
952 child.parent = Some(parent_id);
953 }
954 }
955 }
956 for (parent_id, element_ids) in quick_containment_links {
957 if !spatial_nodes.contains_key(&parent_id) {
958 continue;
959 }
960 for child_id in element_ids {
961 if spatial_nodes.contains_key(&child_id) {
968 let already_placed = spatial_nodes
971 .get(&child_id)
972 .is_some_and(|child| child.parent.is_some());
973 if !already_placed {
974 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
975 parent.children.push(child_id);
976 }
977 if let Some(child) = spatial_nodes.get_mut(&child_id) {
978 child.parent = Some(parent_id);
979 }
980 }
981 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
982 parent.elements.push(child_id);
983 }
984 }
985 }
986 for (parent_id, element_ids) in quick_referenced_links {
990 if !spatial_nodes.contains_key(&parent_id) {
991 continue;
992 }
993 for child_id in element_ids {
994 if spatial_nodes.contains_key(&child_id) {
997 continue;
998 }
999 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1000 parent.elements.push(child_id);
1001 }
1002 }
1003 }
1004 let mut root_id = spatial_nodes
1005 .values()
1006 .find(|node| node.type_name == "IfcProject")
1007 .map(|node| node.express_id);
1008 if root_id.is_none() {
1009 root_id = spatial_nodes
1010 .values()
1011 .find(|node| node.parent.is_none())
1012 .map(|node| node.express_id);
1013 }
1014 let spatial_tree = root_id
1015 .map(|root| {
1016 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1017 })
1018 .transpose()
1019 .unwrap_or(None);
1020 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1021 schema_version: schema_version.clone(),
1022 entity_count: total_entities,
1023 spatial_tree,
1024 });
1025 }
1026
1027 let preprocess_start = Clock::now();
1029 let preprocess_span =
1030 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1031 let unit_scales = tracing::debug_span!("unit_scale")
1037 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1038 tracing::debug!(
1039 length_unit_scale = unit_scales.length_unit_scale,
1040 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1041 "Resolved unit scales"
1042 );
1043 decoder.seed_unit_scales(
1044 unit_scales.length_unit_scale,
1045 unit_scales.plane_angle_to_radians,
1046 );
1047 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1048 router.set_tessellation_quality(options.tessellation_quality);
1049 let material_spans = &prepass_spans.rel_associates_material;
1053 router.set_material_layer_index(Arc::new(
1054 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1055 ));
1056
1057 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1059 let entity = decoder.decode_at(start, end).ok()?;
1060 let matrix = router
1061 .resolve_scaled_placement(&entity, &mut decoder)
1062 .ok()?;
1063 Some(matrix.to_vec())
1064 });
1065 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1066 let entity = decoder.decode_at(start, end).ok()?;
1067 let matrix = router
1068 .resolve_scaled_placement(&entity, &mut decoder)
1069 .ok()?;
1070 Some(matrix.to_vec())
1071 });
1072
1073 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1074 .iter()
1075 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1076 .collect();
1077 let detected_rtc_offset =
1078 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1079
1080 let site_rtc = site_transform
1089 .as_ref()
1090 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1092 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1093 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1094 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1095 } else if detected_has_offset {
1096 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1097 } else {
1098 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1099 };
1100 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1101 router.set_rtc_offset(rtc_offset);
1102 let preprocess_time = preprocess_start.elapsed();
1103 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1104 drop(preprocess_span);
1105
1106 let parse_time = parse_start.elapsed();
1107 tracing::info!(
1108 entity_scan_time_ms = entity_scan_time.as_millis(),
1109 lookup_time_ms = lookup_time.as_millis(),
1110 preprocess_time_ms = preprocess_time.as_millis(),
1111 parse_time_ms = parse_time.as_millis(),
1112 "Parse phase complete, starting geometry extraction"
1113 );
1114
1115 let geometry_start = Clock::now();
1117 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1119 let rtc_offset = router.rtc_offset();
1120 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1128 let void_index_arc = Arc::new(filtered_void_index);
1129 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1130 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1133 let mut geometry_style_index = Arc::new(geometry_style_index);
1134 let indexed_colour_full = Arc::new(indexed_colour_full);
1135 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1140 content,
1141 &mut decoder,
1142 ));
1143 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1147 .iter()
1148 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1149 .collect();
1150 let element_material_colors = Arc::new(element_material_colors);
1151
1152 let total_jobs = entity_jobs.len();
1153 let initial_chunk_size = options.initial_batch_size.max(1);
1154 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1155 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1156 FxHashMap::default();
1157 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1158 FxHashMap::default();
1159 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1160 let mut meshes: Vec<MeshData> = Vec::new();
1161 let mut processed_jobs = 0usize;
1162 let mut total_meshes = 0usize;
1163 let mut total_vertices = 0usize;
1164 let mut total_triangles = 0usize;
1165 let mut chunk_start = 0usize;
1166 let mut current_chunk_size = initial_chunk_size;
1167
1168 let mut deferred_styles_applied = !defer_style_updates;
1169
1170 let csg_failure_collector: std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>> =
1173 std::sync::Mutex::new(FxHashMap::default());
1174 let classification_collector: std::sync::Mutex<ifc_lite_geometry::ClassificationStats> =
1181 std::sync::Mutex::new(ifc_lite_geometry::ClassificationStats::default());
1182 let host_diag_collector: std::sync::Mutex<FxHashMap<u32, ifc_lite_geometry::HostOpeningDiagnostic>> =
1183 std::sync::Mutex::new(FxHashMap::default());
1184 let rect_fast_collector: std::sync::Mutex<ifc_lite_geometry::RectFastStats> =
1188 std::sync::Mutex::new(ifc_lite_geometry::RectFastStats::default());
1189 let backstop_collector = std::sync::atomic::AtomicU64::new(0);
1194
1195 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1201
1202 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1208
1209 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1229 && options.retain_emitted_meshes
1230 && coord_space != SITE_LOCAL_MESH_COORDINATE_SPACE)
1231 .then(|| {
1232 Arc::new(
1233 mapped_item_plan
1234 .into_iter()
1235 .filter(|(_, (count, _))| *count >= 2)
1236 .collect::<FxHashMap<u32, (u32, u32)>>(),
1237 )
1238 });
1239 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1258 && !indexed_colour_full.is_empty())
1259 .then(|| {
1260 let ids: FxHashSet<u32> = indexed_colour_full
1261 .iter()
1262 .filter(|(_, m)| m.has_multiple_colours())
1263 .map(|(&id, _)| id)
1264 .collect();
1265 (!ids.is_empty()).then(|| Arc::new(ids))
1266 })
1267 .flatten();
1268 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1271 std::sync::Mutex::new(Vec::new());
1272
1273 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1281 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1282 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1283
1284 let worker_point_caches = jobs::new_worker_point_caches();
1285 let worker_placement_caches = jobs::new_worker_placement_caches();
1289
1290 let geometry_span = tracing::info_span!(
1291 "geometry",
1292 element_count = total_jobs,
1293 mesh_count = tracing::field::Empty,
1294 triangle_count = tracing::field::Empty,
1295 backstop_count = tracing::field::Empty,
1296 total_csg_failures = tracing::field::Empty,
1297 phase_ms = tracing::field::Empty,
1298 );
1299 let geometry_guard = geometry_span.clone().entered();
1300
1301 while chunk_start < total_jobs {
1302 if options
1306 .cancel
1307 .as_ref()
1308 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1309 {
1310 break;
1311 }
1312 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1313 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1314
1315 #[cfg(not(target_arch = "wasm32"))]
1319 {
1320 let entity_index_for_meta = entity_index_arc.clone();
1322 jobs_chunk.par_iter_mut().for_each(|job| {
1323 if job.global_id.is_some()
1324 || job.name.is_some()
1325 || job.product_definition_shape_id.is_some()
1326 {
1327 return;
1328 }
1329 let mut local_decoder =
1330 EntityDecoder::with_arc_index(content, entity_index_for_meta.clone());
1331 let Ok(entity) = local_decoder.decode_at(job.start, job.end) else {
1332 return;
1333 };
1334 job.global_id = normalize_optional_string(entity.get_string(0));
1335 job.name = normalize_optional_string(entity.get_string(2));
1336 job.product_definition_shape_id = entity.get_ref(6);
1337 });
1338
1339 for job in jobs_chunk.iter_mut() {
1341 let Some(pds_id) = job.product_definition_shape_id else {
1342 continue;
1343 };
1344 let resolved_color = color_cache_by_product_definition_shape
1345 .entry(pds_id)
1346 .or_insert_with(|| {
1347 resolve_element_color_for_product_definition_shape(
1348 pds_id,
1349 &geometry_style_index,
1350 &mut decoder,
1351 )
1352 });
1353 if let Some(color) = resolved_color {
1354 job.element_color = *color;
1355 } else if let Some(color) = element_material_color.get(&job.id) {
1356 job.element_color = *color;
1359 }
1360 if options.include_presentation_layers {
1361 let resolved_layer = layer_cache_by_product_definition_shape
1362 .entry(pds_id)
1363 .or_insert_with(|| {
1364 resolve_presentation_layer_for_product_definition_shape(
1365 pds_id,
1366 &presentation_layer_by_assigned_id,
1367 &mut layer_cache_by_representation,
1368 &mut decoder,
1369 )
1370 });
1371 job.presentation_layer = resolved_layer.clone();
1372 }
1373 }
1374 }
1375
1376 #[cfg(target_arch = "wasm32")]
1378 for job in jobs_chunk.iter_mut() {
1379 populate_entity_job_metadata(
1380 job,
1381 &geometry_style_index,
1382 &element_material_color,
1383 &presentation_layer_by_assigned_id,
1384 &mut color_cache_by_product_definition_shape,
1385 &mut layer_cache_by_product_definition_shape,
1386 &mut layer_cache_by_representation,
1387 &mut decoder,
1388 options.include_presentation_layers,
1389 );
1390 }
1391 let site_local_rotation: Option<&Vec<f64>> =
1392 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1393 site_transform.as_ref()
1394 } else {
1395 None
1396 };
1397 let chunk_meshes: Vec<MeshData> = jobs_chunk
1402 .par_iter()
1403 .map(|job| {
1404 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1405 let mut fallback_cache = FxHashMap::default();
1415 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1416 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1417 match slot_guard.as_deref_mut() {
1418 Some(cache) => cache,
1419 None => &mut fallback_cache,
1420 };
1421 let mut fallback_placement_cache = FxHashMap::default();
1430 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1431 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1432 match placement_slot_guard.as_deref_mut() {
1433 Some(cache) => cache,
1434 None => &mut fallback_placement_cache,
1435 };
1436 process_entity_job(
1437 job,
1438 content,
1439 &entity_index_arc,
1440 unit_scale,
1441 rtc_offset,
1442 seed_plane_angle_to_radians,
1443 options.tessellation_quality,
1444 void_index_arc.as_ref(),
1445 skipped_entity_ids.as_ref(),
1446 geometry_style_index.as_ref(),
1447 indexed_colour_full.as_ref(),
1448 element_material_colors.as_ref(),
1449 texture_index.as_ref(),
1450 site_local_rotation,
1451 &csg_failure_collector,
1452 &classification_collector,
1453 &host_diag_collector,
1454 &rect_fast_collector,
1455 &backstop_collector,
1456 &item_dedup_cache,
1457 &mapped_item_cache,
1458 instancing_plan.as_ref(),
1459 indexed_colour_split_ids.as_ref(),
1460 &raw_instance_collector,
1461 worker_point_cache,
1462 worker_placement_cache,
1463 &point_cache_hits_collector,
1464 &point_cache_misses_collector,
1465 &faceted_brep_ns_collector,
1466 )
1467 })
1468 .flatten_iter()
1469 .collect();
1470
1471 processed_jobs += jobs_chunk.len();
1472 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1473 total_triangles += chunk_meshes
1474 .iter()
1475 .map(|m| m.triangle_count())
1476 .sum::<usize>();
1477
1478 if !chunk_meshes.is_empty() {
1479 total_meshes += chunk_meshes.len();
1480 let emit_mesh_chunk_size = current_chunk_size.max(1);
1481 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1482 on_batch(emitted_meshes, processed_jobs, total_jobs);
1483 }
1484 if options.retain_emitted_meshes {
1485 meshes.extend(chunk_meshes);
1486 }
1487
1488 if !deferred_styles_applied {
1489 let mut rebuilt_styles = {
1494 let mut style_decoder =
1495 EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1496 crate::prepass::resolve_styled_item_spans(
1497 &deferred_styled_item_positions,
1498 &mut style_decoder,
1499 )
1500 };
1501 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1502 geometry_style_index = Arc::new(rebuilt_styles);
1503 let deferred_color_updates = build_color_updates_for_jobs(
1504 &entity_jobs[..processed_jobs],
1505 geometry_style_index.as_ref(),
1506 content,
1507 &entity_index_arc,
1508 );
1509 if !deferred_color_updates.is_empty() {
1510 on_color_update(&deferred_color_updates);
1511 }
1512 deferred_styles_applied = true;
1513 }
1514 }
1515 chunk_start = chunk_end;
1516 current_chunk_size = throughput_chunk_size;
1517 }
1518
1519 let geometry_time = geometry_start.elapsed();
1520 let csg_failures = csg_failure_collector
1523 .into_inner()
1524 .unwrap_or_else(|poisoned| poisoned.into_inner());
1525 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1526 let products_with_failures = csg_failures.len();
1527 let backstop_dropped = backstop_collector.into_inner();
1528 let point_cache_hits = point_cache_hits_collector.into_inner();
1529 let point_cache_misses = point_cache_misses_collector.into_inner();
1530 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1531 geometry_span.record("mesh_count", total_meshes as u64);
1532 geometry_span.record("triangle_count", total_triangles as u64);
1533 geometry_span.record("backstop_count", backstop_dropped);
1534 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1535 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1536 drop(geometry_guard);
1537 if total_csg_failures > 0 {
1538 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1539 for fails in csg_failures.values() {
1540 for f in fails {
1541 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1542 }
1543 }
1544 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1545 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1546 let breakdown = breakdown
1547 .iter()
1548 .map(|(reason, count)| format!("{reason}={count}"))
1549 .collect::<Vec<_>>()
1550 .join(" ");
1551 tracing::warn!(
1552 total_csg_failures,
1553 products_with_failures,
1554 %breakdown,
1555 "CSG failures during geometry extraction (cut dropped, host kept uncut)"
1556 );
1557 }
1558
1559 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1568 const WORST_HOSTS_LIMIT: usize = 16;
1570 let classification = classification_collector
1571 .into_inner()
1572 .unwrap_or_else(|poisoned| poisoned.into_inner());
1573 let host_diags = host_diag_collector
1574 .into_inner()
1575 .unwrap_or_else(|poisoned| poisoned.into_inner());
1576 let rect_fast = rect_fast_collector
1577 .into_inner()
1578 .unwrap_or_else(|poisoned| poisoned.into_inner());
1579 let diag = ifc_lite_geometry::aggregate_diagnostics(
1580 classification,
1581 &csg_failures,
1582 &host_diags,
1583 rect_fast,
1584 WORST_HOSTS_LIMIT,
1585 );
1586 (!diag.is_empty()).then_some(diag)
1587 });
1588
1589 let instances = instancing::finalize_instances(
1594 raw_instance_collector
1595 .into_inner()
1596 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1597 &mut meshes,
1598 &mapped_item_cache,
1599 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1600 );
1601
1602 let total_time = total_start.elapsed();
1603 pipeline_span.record("element_count", total_jobs as u64);
1604 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1605
1606 tracing::info!(
1607 meshes = meshes.len(),
1608 instances = instances.len(),
1609 vertices = total_vertices,
1610 triangles = total_triangles,
1611 backstop_count = backstop_dropped,
1612 geometry_time_ms = geometry_time.as_millis(),
1613 total_time_ms = total_time.as_millis(),
1614 "Geometry processing complete"
1615 );
1616
1617 ProcessingResult {
1618 meshes,
1619 instances,
1620 mesh_coordinate_space: Some(coord_space.to_string()),
1621 site_transform,
1622 building_transform,
1623 metadata: ModelMetadata {
1624 schema_version,
1625 entity_count: total_entities,
1626 geometry_entity_count,
1627 coordinate_info: CoordinateInfo {
1628 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1629 is_geo_referenced: has_rtc_offset,
1630 },
1631 length_unit_scale: Some(unit_scale),
1632 georeferencing: crate::extract_georeferencing_with_index(content, &entity_index_arc),
1633 },
1634 stats: ProcessingStats {
1635 total_meshes,
1636 total_vertices,
1637 total_triangles,
1638 parse_time_ms: parse_time.as_millis() as u64,
1639 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1640 lookup_time_ms: lookup_time.as_millis() as u64,
1641 preprocess_time_ms: preprocess_time.as_millis() as u64,
1642 geometry_time_ms: geometry_time.as_millis() as u64,
1643 total_time_ms: total_time.as_millis() as u64,
1644 from_cache: false,
1645 total_csg_failures: total_csg_failures as u64,
1646 products_with_failures: products_with_failures as u64,
1647 degenerate_triangles_dropped: backstop_dropped,
1648 point_cache_hits,
1649 point_cache_misses,
1650 faceted_brep_time_ms,
1651 geometry_diagnostics,
1652 },
1653 }
1654}
1655
1656