1use crate::types::mesh::{InstanceRecord, MeshData, RawInstanceOccurrence};
10use crate::types::response::{
11 CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
12 QuickMetadataEntitySummary,
13};
14use ifc_lite_core::{
15 keyword_eq, keyword_starts_with, EntityDecoder,
16 EntityIndex, EntityScanner, IfcType,
17};
18use ifc_lite_geometry::TessellationQuality;
19use ifc_lite_geometry::GeometryRouter;
20use rayon::prelude::*;
21use rustc_hash::{FxHashMap, FxHashSet};
22use std::collections::{BTreeMap, HashMap, HashSet};
23use std::sync::Arc;
24
25mod color_layer;
26mod csg_summary;
27#[cfg(test)]
28#[path = "csg_summary_tests.rs"]
29mod csg_summary_tests;
30mod diagnostics;
31mod entity_index;
32use entity_index::{IndexBuilder, ProcessingIndex};
33mod id_filter;
34pub use id_filter::{
35 process_geometry_filtered_with_quality, process_geometry_filtered_with_quality_and_ids,
36};
37mod baked_basis;
38pub use baked_basis::process_geometry_streaming_filtered_with_baked_basis;
39use baked_basis::PassScope;
40pub(crate) mod instancing;
41mod jobs;
42mod opening_filter;
43mod phase_marks;
44use phase_marks::{mark, Mark};
45mod properties;
46mod quick_metadata;
47mod schema_detection;
48mod site_local;
49
50pub use quick_metadata::is_quick_spatial_type_ci;
51pub use site_local::{convert_mesh_to_site_local, native_to_baked};
52pub(crate) use site_local::site_local_rotation_invalidates_captured_transforms;
53
54use jobs::{build_color_updates_for_jobs, process_entity_job};
55
56pub(crate) use color_layer::resolve_element_color_for_product_definition_shape;
57use color_layer::{
58 collect_presentation_layer_assignments,
59 resolve_presentation_layer_for_product_definition_shape,
60};
61use opening_filter::apply_opening_filter;
62use properties::resolve_space_zone_properties_lazy;
63use quick_metadata::{
64 build_quick_spatial_tree_node, extract_name_from_args, extract_storey_elevation_from_args,
65 parse_step_arguments, parse_step_ref, parse_step_ref_list, QuickSpatialNodeEntry,
66};
67use crate::mesh_frame::{MeshCoordinateSpace, MeshFrame};
68
69#[cfg(not(target_arch = "wasm32"))]
76use std::time::Instant as Clock;
77
78#[cfg(target_arch = "wasm32")]
79#[derive(Clone, Copy)]
80struct Clock;
81
82#[cfg(target_arch = "wasm32")]
83impl Clock {
84 #[inline]
85 fn now() -> Self {
86 Clock
87 }
88 #[inline]
89 fn elapsed(&self) -> std::time::Duration {
90 std::time::Duration::ZERO
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum OpeningFilterMode {
98 #[default]
100 Default = 0,
101 IgnoreAll = 1,
103 IgnoreOpaque = 2,
105}
106
107impl OpeningFilterMode {
108 pub fn cache_key_suffix(&self) -> &'static str {
111 match self {
112 Self::Default => "default",
113 Self::IgnoreAll => "ignore_all",
114 Self::IgnoreOpaque => "ignore_opaque",
115 }
116 }
117}
118
119pub struct ProcessingResult {
121 pub meshes: Vec<MeshData>,
122 pub instances: Vec<InstanceRecord>,
129 pub mesh_coordinate_space: MeshCoordinateSpace,
135 pub frame: MeshFrame,
144 pub site_transform: Option<Vec<f64>>,
146 pub building_transform: Option<Vec<f64>>,
148 pub metadata: ModelMetadata,
149 pub stats: ProcessingStats,
150}
151
152#[derive(Debug, Clone)]
156pub struct StreamingOptions {
157 pub initial_batch_size: usize,
159 pub throughput_batch_size: usize,
161 pub fast_first_batch: bool,
163 pub include_properties: bool,
165 pub include_presentation_layers: bool,
167 pub emit_quick_metadata_bootstrap: bool,
169 pub retain_emitted_meshes: bool,
171 pub tessellation_quality: TessellationQuality,
178 pub entity_index: Option<Arc<EntityIndex>>,
186 pub cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
192 pub enable_instancing: bool,
201}
202
203impl Default for StreamingOptions {
204 fn default() -> Self {
205 Self {
206 initial_batch_size: 50,
207 throughput_batch_size: 50,
208 fast_first_batch: false,
209 include_properties: true,
210 include_presentation_layers: true,
211 emit_quick_metadata_bootstrap: false,
212 retain_emitted_meshes: true,
213 tessellation_quality: TessellationQuality::default(),
214 entity_index: None,
215 cancel: None,
216 enable_instancing: false,
217 }
218 }
219}
220
221pub(super) struct EntityJob {
223 pub(super) id: u32,
224 pub(super) ifc_type: IfcType,
225 pub(super) start: usize,
226 pub(super) end: usize,
227 pub(super) product_definition_shape_id: Option<u32>,
228 pub(super) element_color: [f32; 4],
229 pub(super) global_id: Option<String>,
230 pub(super) name: Option<String>,
231 pub(super) presentation_layer: Option<String>,
232 pub(super) space_zone_properties: Option<BTreeMap<String, String>>,
233 pub(super) representation_map_id: Option<u32>,
237}
238
239#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
241#[allow(clippy::too_many_arguments)]
243fn populate_entity_job_metadata(
244 job: &mut EntityJob,
245 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
246 element_material_color: &FxHashMap<u32, [f32; 4]>,
247 layer_by_assigned_representation: &FxHashMap<u32, String>,
248 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
249 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
250 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
251 decoder: &mut EntityDecoder,
252 include_presentation_layers: bool,
253) {
254 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
255 return;
256 }
257
258 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
259 return;
260 };
261
262 job.global_id = normalize_optional_string(entity.get_string(0));
263 job.name = normalize_optional_string(entity.get_string(2));
264 job.product_definition_shape_id = entity.get_ref(6);
265
266 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
267 return;
268 };
269
270 let resolved_color = color_cache_by_product_definition_shape
271 .entry(product_definition_shape_id)
272 .or_insert_with(|| {
273 resolve_element_color_for_product_definition_shape(
274 product_definition_shape_id,
275 geometry_style_index,
276 decoder,
277 )
278 });
279 if let Some(color) = resolved_color {
280 job.element_color = *color;
281 } else if let Some(color) = element_material_color.get(&job.id) {
282 job.element_color = *color;
283 }
284
285 if include_presentation_layers {
286 let resolved_layer = layer_cache_by_product_definition_shape
287 .entry(product_definition_shape_id)
288 .or_insert_with(|| {
289 resolve_presentation_layer_for_product_definition_shape(
290 product_definition_shape_id,
291 layer_by_assigned_representation,
292 layer_cache_by_representation,
293 decoder,
294 )
295 });
296 job.presentation_layer = resolved_layer.clone();
297 }
298}
299
300use crate::style::GeometryStyleInfo;
304
305pub(super) fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
306 let value = raw?.trim();
307 if value.is_empty() || value == "$" {
308 return None;
309 }
310 Some(value.to_string())
311}
312
313fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
314 match ifc_type {
315 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
316 IfcType::IfcSlab => 95,
317 IfcType::IfcColumn => 90,
318 IfcType::IfcBeam => 85,
319 IfcType::IfcRoof => 80,
320 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
321 IfcType::IfcCurtainWall => 70,
322 IfcType::IfcFooting | IfcType::IfcPile => 65,
323 IfcType::IfcDoor | IfcType::IfcWindow => 30,
324 IfcType::IfcFurnishingElement => 10,
325 _ => 50,
326 }
327}
328
329pub fn process_geometry<T>(content: &T) -> ProcessingResult
331where
332 T: AsRef<[u8]> + ?Sized,
333{
334 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
335}
336
337pub fn process_geometry_with_index<T>(content: &T, index: Arc<EntityIndex>) -> ProcessingResult
344where
345 T: AsRef<[u8]> + ?Sized,
346{
347 process_geometry_streaming_filtered_with_options(
348 content.as_ref(),
349 OpeningFilterMode::Default,
350 StreamingOptions {
351 initial_batch_size: usize::MAX,
352 throughput_batch_size: usize::MAX,
353 entity_index: Some(index),
354 ..StreamingOptions::default()
355 },
356 |_, _, _| {},
357 |_| {},
358 |_| {},
359 )
360}
361
362pub fn process_geometry_streaming(
364 content: &[u8],
365 batch_size: usize,
366 on_batch: impl FnMut(&[MeshData], usize, usize),
367) -> ProcessingResult {
368 process_geometry_streaming_with_options(
369 content,
370 StreamingOptions {
371 initial_batch_size: batch_size,
372 throughput_batch_size: batch_size,
373 ..StreamingOptions::default()
374 },
375 on_batch,
376 |_| {},
377 )
378}
379
380pub fn process_geometry_streaming_with_options(
382 content: &[u8],
383 options: StreamingOptions,
384 on_batch: impl FnMut(&[MeshData], usize, usize),
385 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
386) -> ProcessingResult {
387 process_geometry_streaming_with_options_and_bootstrap(
388 content,
389 options,
390 on_batch,
391 on_color_update,
392 |_| {},
393 )
394}
395
396pub fn process_geometry_streaming_with_options_and_bootstrap(
399 content: &[u8],
400 options: StreamingOptions,
401 on_batch: impl FnMut(&[MeshData], usize, usize),
402 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
403 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
404) -> ProcessingResult {
405 process_geometry_streaming_filtered_with_options(
406 content,
407 OpeningFilterMode::Default,
408 options,
409 on_batch,
410 on_color_update,
411 on_quick_metadata_bootstrap,
412 )
413}
414
415pub fn process_geometry_filtered<T>(
417 content: &T,
418 opening_filter: OpeningFilterMode,
419) -> ProcessingResult
420where
421 T: AsRef<[u8]> + ?Sized,
422{
423 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
424}
425
426pub fn process_geometry_streaming_filtered(
428 content: &[u8],
429 opening_filter: OpeningFilterMode,
430 batch_size: usize,
431 on_batch: impl FnMut(&[MeshData], usize, usize),
432 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
433) -> ProcessingResult {
434 process_geometry_streaming_filtered_with_options(
435 content,
436 opening_filter,
437 StreamingOptions {
438 initial_batch_size: batch_size,
439 throughput_batch_size: batch_size,
440 ..StreamingOptions::default()
441 },
442 on_batch,
443 on_color_update,
444 |_| {},
445 )
446}
447
448pub fn process_geometry_streaming_filtered_with_options(
450 content: &[u8],
451 opening_filter: OpeningFilterMode,
452 options: StreamingOptions,
453 on_batch: impl FnMut(&[MeshData], usize, usize),
454 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
455 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
456) -> ProcessingResult {
457 process_geometry_streaming_filtered_with_options_and_ids(
458 content,
459 opening_filter,
460 options,
461 PassScope::default(),
462 on_batch,
463 on_color_update,
464 on_quick_metadata_bootstrap,
465 )
466}
467
468fn process_geometry_streaming_filtered_with_options_and_ids(
469 content: &[u8],
470 opening_filter: OpeningFilterMode,
471 options: StreamingOptions,
472 PassScope { entity_ids, baked_basis_out }: PassScope<'_>,
473 mut on_batch: impl FnMut(&[MeshData], usize, usize),
474 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
475 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
476) -> ProcessingResult {
477 let total_start = mark(Mark::PipelineStart, Clock::now());
478 let parse_start = Clock::now();
479 let entity_scan_start = Clock::now();
480
481 let pipeline_span = tracing::info_span!(
488 "geometry_pipeline",
489 byte_size = content.len(),
490 element_count = tracing::field::Empty,
491 total_ms = tracing::field::Empty,
492 );
493 let _pipeline_guard = pipeline_span.clone().entered();
494
495 tracing::info!(
496 content_size = content.len(),
497 "Starting IFC geometry processing"
498 );
499
500 let scan_span = tracing::info_span!(
501 "scan_prepass",
502 total_entities = tracing::field::Empty,
503 geometry_entities = tracing::field::Empty,
504 phase_ms = tracing::field::Empty,
505 );
506 let scan_guard = scan_span.clone().entered();
507
508 let provided_index = options.entity_index.clone();
515 let building_index = provided_index.is_none();
516 let mut inline_index = IndexBuilder::new(content.len(), building_index);
517 let mut decoder = match &provided_index {
518 Some(idx) => EntityDecoder::with_arc_index(content, idx.clone()),
519 None => EntityDecoder::new(content),
520 };
521 tracing::debug!("Entity index will be built inline during the scan");
522
523 let mut prepass_spans = crate::prepass::PrepassSpans::default();
529 let mut project_id: Option<u32> = None;
530 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
531 let mut rel_defines_spans: Vec<(usize, usize)> = Vec::new();
538
539 let mut scanner = EntityScanner::new(content);
541 let mut georeferencing_candidates = Vec::new();
542 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
543 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
548 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
549 let mut mapped_item_plan: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
554 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
563 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
564 let mut quick_spatial_nodes =
565 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
566 let mut quick_aggregate_links = if quick_metadata_enabled {
567 Vec::<(u32, Vec<u32>)>::new()
568 } else {
569 Vec::new()
570 };
571 let mut quick_containment_links = if quick_metadata_enabled {
572 Vec::<(u32, Vec<u32>)>::new()
573 } else {
574 Vec::new()
575 };
576 let mut quick_referenced_links = if quick_metadata_enabled {
581 Vec::<(u32, Vec<u32>)>::new()
582 } else {
583 Vec::new()
584 };
585 let mut quick_element_summaries = if quick_metadata_enabled {
586 HashMap::<u32, QuickMetadataEntitySummary>::new()
587 } else {
588 HashMap::new()
589 };
590 let mut total_entities = 0usize;
591 let mut site_entity_pos: Option<(usize, usize)> = None;
592 let mut building_entity_pos: Option<(usize, usize)> = None;
593
594 let defer_style_updates = options.fast_first_batch
595 && opening_filter == OpeningFilterMode::Default
596 && !options.include_presentation_layers;
597
598 while let Some((id, type_name, start, end)) = scanner.next_entity() {
599 total_entities += 1;
600 if let Some(ifc_type) = crate::georeferencing::georeferencing_candidate_type(type_name) {
601 georeferencing_candidates.push((id, ifc_type));
602 }
603 if building_index {
604 inline_index.insert(id, (start, end));
605 }
606 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
607 if is_quick_spatial_type_ci(type_name) {
609 let args = parse_step_arguments(&content[start..end]);
610 let fallback = format!("{type_name} #{id}");
611 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
612 express_id: id,
613 type_name: type_name.to_string(),
614 name: extract_name_from_args(&args, &fallback),
615 elevation: if keyword_eq(type_name, "IFCBUILDINGSTOREY") {
616 extract_storey_elevation_from_args(&args)
617 } else {
618 None
619 },
620 children: Vec::new(),
621 contained: Vec::new(),
622 elements: Vec::new(),
623 named_as_child: false,
624 });
625 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
626 let args = parse_step_arguments(&content[start..end]);
627 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
628 quick_aggregate_links.push((
629 parent_id,
630 args.get(5)
631 .map(|token| parse_step_ref_list(token))
632 .unwrap_or_default(),
633 ));
634 }
635 } else if keyword_eq(type_name, "IFCRELCONTAINEDINSPATIALSTRUCTURE") {
636 let args = parse_step_arguments(&content[start..end]);
637 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
638 quick_containment_links.push((
639 parent_id,
640 args.get(4)
641 .map(|token| parse_step_ref_list(token))
642 .unwrap_or_default(),
643 ));
644 }
645 } else if keyword_eq(type_name, "IFCRELREFERENCEDINSPATIALSTRUCTURE") {
646 let args = parse_step_arguments(&content[start..end]);
647 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
648 quick_referenced_links.push((
649 parent_id,
650 args.get(4)
651 .map(|token| parse_step_ref_list(token))
652 .unwrap_or_default(),
653 ));
654 }
655 }
656 }
657
658 if keyword_eq(type_name, "IFCINDEXEDCOLOURMAP") {
659 prepass_spans.indexed_colour_maps.push((id, start, end));
661 continue;
662 }
663
664 if keyword_eq(type_name, "IFCSTYLEDITEM") {
665 prepass_spans.styled_items.push((id, start, end));
669 continue;
670 } else if keyword_eq(type_name, "IFCMATERIALDEFINITIONREPRESENTATION") {
671 prepass_spans.material_def_reprs.push((id, start, end));
672 continue;
673 } else if keyword_eq(type_name, "IFCRELASSOCIATESMATERIAL") {
674 prepass_spans.rel_associates_material.push((id, start, end));
675 continue;
676 } else if keyword_eq(type_name, "IFCPRESENTATIONLAYERASSIGNMENT") {
677 if !options.include_presentation_layers {
678 continue;
679 }
680 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
681 collect_presentation_layer_assignments(
682 &mut presentation_layer_by_assigned_id,
683 &layer_assignment,
684 );
685 }
686 continue;
687 } else if keyword_eq(type_name, "IFCPROPERTYSET") {
688 continue;
691 } else if keyword_eq(type_name, "IFCRELDEFINESBYPROPERTIES") {
692 if options.include_properties {
693 rel_defines_spans.push((start, end));
694 }
695 continue;
696 } else if keyword_starts_with(type_name, "IFCPROPERTY") {
697 continue;
700 } else if keyword_eq(type_name, "IFCRELVOIDSELEMENT") {
701 prepass_spans.void_rels.push((id, start, end));
702 } else if keyword_eq(type_name, "IFCRELFILLSELEMENT") {
703 prepass_spans.fills_rels.push((id, start, end));
704 } else if keyword_eq(type_name, "IFCRELAGGREGATES") {
705 prepass_spans.aggregate_rels.push((id, start, end));
710 } else if keyword_eq(type_name, "IFCPROJECT") && project_id.is_none() {
711 project_id = Some(id);
712 } else if keyword_eq(type_name, "IFCSITE") && site_entity_pos.is_none() {
713 site_entity_pos = Some((start, end));
714 } else if keyword_eq(type_name, "IFCBUILDING") && building_entity_pos.is_none() {
715 building_entity_pos = Some((start, end));
716 }
717
718 let (has_geometry, representationless_spatial) =
719 ifc_lite_core::geometry_flags_by_name(type_name);
720 if has_geometry {
721 let ifc_type = ifc_lite_core::ifc_type_from_keyword(type_name);
725 if quick_metadata_enabled {
726 quick_element_summaries.insert(
727 id,
728 QuickMetadataEntitySummary {
729 express_id: id,
730 type_name: type_name.to_string(),
731 name: format!("{type_name} #{id}"),
732 global_id: None,
733 kind: "element".to_string(),
734 has_children: false,
735 element_count: None,
736 elevation: None,
737 },
738 );
739 }
740 entity_jobs.push(EntityJob {
741 id,
742 ifc_type: ifc_type.clone(),
743 start,
744 end,
745 product_definition_shape_id: None,
746 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
747 global_id: None,
748 name: None,
749 presentation_layer: None,
750 space_zone_properties: None,
751 representation_map_id: None,
752 });
753 } else if representationless_spatial
754 && ifc_lite_core::nth_attribute_is_present(&content[start..end], 6)
755 {
756 let ifc_type = ifc_lite_core::ifc_type_from_keyword(type_name);
768 entity_jobs.push(EntityJob {
769 id,
770 ifc_type: ifc_type.clone(),
771 start,
772 end,
773 product_definition_shape_id: None,
774 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
775 global_id: None,
776 name: None,
777 presentation_layer: None,
778 space_zone_properties: None,
779 representation_map_id: None,
780 });
781 }
782 else if keyword_eq(type_name, "IFCMAPPEDITEM") {
789 let args = parse_step_arguments(&content[start..end]);
790 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
791 referenced_representation_maps.insert(source_id);
792 if options.enable_instancing {
796 mapped_item_plan
797 .entry(source_id)
798 .and_modify(|(count, template)| {
799 *count += 1;
800 if id < *template {
801 *template = id;
802 }
803 })
804 .or_insert((1, id));
805 }
806 }
807 } else if keyword_eq(type_name, "IFCRELDEFINESBYTYPE") {
808 let args = parse_step_arguments(&content[start..end]);
811 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
812 instantiated_type_ids.insert(type_id);
813 }
814 prepass_spans.defines_by_type.push((id, start, end));
816 } else if let Some(type_ty) = ifc_lite_core::type_product_ifc_type(type_name) {
817 let args = parse_step_arguments(&content[start..end]);
818 let rep_map_ids = args
820 .get(6)
821 .map(|token| parse_step_ref_list(token))
822 .unwrap_or_default();
823 if !rep_map_ids.is_empty() {
824 type_product_geometry.push((id, start, end, type_ty, rep_map_ids));
825 }
826 }
827 }
828
829 ifc_lite_core::report_scan_diagnostics(scanner.skipped_oversized_ids(), scanner.malformed_record_start().is_some());
831
832 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
840 for (rep_map_id, _class) in crate::element::plan_type_geometry(
845 rep_map_ids,
846 &referenced_representation_maps,
847 instantiated_type_ids.contains(type_id),
848 crate::element::TypeGeometryMode::SuppressInstanced,
849 ) {
850 entity_jobs.push(EntityJob {
851 id: *type_id,
852 ifc_type: ifc_type.clone(),
853 start: *start,
854 end: *end,
855 product_definition_shape_id: None,
856 element_color: crate::style::default_color_for_type(ifc_type.clone()).to_array(),
857 global_id: None,
858 name: None,
859 presentation_layer: None,
860 space_zone_properties: None,
861 representation_map_id: Some(rep_map_id),
862 });
863 }
864 }
865
866 let entity_index = match provided_index {
871 Some(idx) => ProcessingIndex::hash(idx),
872 None => inline_index.finish(),
873 };
874 entity_index.install(&mut decoder);
875
876 let resolved = crate::prepass::resolve_prepass(
881 &prepass_spans,
882 &mut decoder,
883 crate::prepass::ResolveOptions {
884 collect_indexed_colour_full: true,
885 defer_attached_styles: defer_style_updates,
886 },
887 );
888 let crate::prepass::ResolvedPrepass {
889 mut geometry_style_index,
890 indexed_colour_index,
891 indexed_colour_full,
892 element_material_colors,
893 void_index,
894 filling_by_opening,
895 deferred_attached_styled_spans: deferred_styled_item_positions,
896 ..
897 } = resolved;
898
899 let entity_scan_time = mark(Mark::EntityScanEnd, entity_scan_start.elapsed());
900 scan_span.record("total_entities", total_entities as u64);
901 scan_span.record("geometry_entities", entity_jobs.len() as u64);
902 scan_span.record("phase_ms", entity_scan_time.as_millis() as u64);
903 drop(scan_guard);
904
905 let lookup_start = mark(Mark::LookupStart, Clock::now());
906 let lookup_span = tracing::debug_span!("lookup", phase_ms = tracing::field::Empty).entered();
907 if options.include_properties {
908 resolve_space_zone_properties_lazy(&mut entity_jobs, &mut decoder, &rel_defines_spans);
909 }
910 if options.fast_first_batch {
911 entity_jobs.sort_by(|left, right| {
912 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
913 });
914 }
915 let lookup_time = mark(Mark::LookupEnd, lookup_start.elapsed());
916 lookup_span.record("phase_ms", lookup_time.as_millis() as u64);
917 drop(lookup_span);
918
919 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
923 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
924 &entity_jobs,
925 &void_index,
926 &filling_by_opening,
927 &geometry_style_index,
928 &element_material_colors,
929 &mut decoder,
930 opening_filter,
931 );
932
933 if let Some(ids) = entity_ids {
939 entity_jobs.retain(|job| ids.contains(&job.id));
940 }
941
942 let schema_version = schema_detection::detect_schema_version(content).to_string();
943
944 let geometry_entity_count = entity_jobs.len();
945 tracing::info!(
946 total_entities = total_entities,
947 geometry_entities = geometry_entity_count,
948 voids = void_index.len(),
949 schema_version = %schema_version,
950 "Entity scanning complete"
951 );
952
953 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
954 for (parent_id, child_ids) in quick_aggregate_links {
955 if !spatial_nodes.contains_key(&parent_id) {
956 continue;
957 }
958 for child_id in child_ids {
959 if !spatial_nodes.contains_key(&child_id) {
960 continue;
961 }
962 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
963 parent.children.push(child_id);
964 }
965 if let Some(child) = spatial_nodes.get_mut(&child_id) {
966 child.named_as_child = true;
967 }
968 }
969 }
970 for (parent_id, element_ids) in quick_containment_links {
971 if !spatial_nodes.contains_key(&parent_id) {
972 continue;
973 }
974 for child_id in element_ids {
975 if spatial_nodes.contains_key(&child_id) {
988 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
989 parent.contained.push(child_id);
990 }
991 if let Some(child) = spatial_nodes.get_mut(&child_id) {
992 child.named_as_child = true;
993 }
994 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
995 parent.elements.push(child_id);
996 }
997 }
998 }
999 for (parent_id, element_ids) in quick_referenced_links {
1003 if !spatial_nodes.contains_key(&parent_id) {
1004 continue;
1005 }
1006 for child_id in element_ids {
1007 if spatial_nodes.contains_key(&child_id) {
1010 continue;
1011 }
1012 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1013 parent.elements.push(child_id);
1014 }
1015 }
1016 }
1017 let root_id = spatial_nodes
1021 .values()
1022 .filter(|node| keyword_eq(&node.type_name, "IFCPROJECT"))
1023 .map(|node| node.express_id)
1024 .min()
1025 .or_else(|| {
1026 spatial_nodes
1030 .values()
1031 .min_by_key(|node| (node.named_as_child, node.express_id))
1032 .map(|node| node.express_id)
1033 });
1034 let (spatial_tree, pruned_aggregate_edges) = root_id
1035 .and_then(|root| {
1036 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries).ok()
1037 })
1038 .unzip();
1039 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1040 schema_version: schema_version.clone(),
1041 entity_count: total_entities,
1042 spatial_tree,
1043 pruned_aggregate_edges: pruned_aggregate_edges.unwrap_or_default(),
1044 });
1045 }
1046
1047 let preprocess_start = mark(Mark::PreprocessStart, Clock::now());
1049 let preprocess_span =
1050 tracing::debug_span!("preprocess", phase_ms = tracing::field::Empty).entered();
1051 let unit_scales = tracing::debug_span!("unit_scale")
1057 .in_scope(|| crate::prepass::resolve_unit_scales(content, project_id, &mut decoder));
1058 tracing::debug!(
1059 length_unit_scale = unit_scales.length_unit_scale,
1060 plane_angle_to_radians = unit_scales.plane_angle_to_radians,
1061 "Resolved unit scales"
1062 );
1063 decoder.seed_unit_scales(
1064 unit_scales.length_unit_scale,
1065 unit_scales.plane_angle_to_radians,
1066 );
1067 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1069 router.set_tessellation_quality(options.tessellation_quality);
1070 let material_spans = &prepass_spans.rel_associates_material;
1074 router.set_material_layer_index(Arc::new(
1075 ifc_lite_geometry::MaterialLayerIndex::from_spans(material_spans, &mut decoder),
1076 ));
1077
1078 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1080 let entity = decoder.decode_at(start, end).ok()?;
1081 let matrix = router
1082 .resolve_scaled_placement(&entity, &mut decoder)
1083 .ok()?;
1084 Some(matrix.to_vec())
1085 });
1086 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1087 let entity = decoder.decode_at(start, end).ok()?;
1088 let matrix = router
1089 .resolve_scaled_placement(&entity, &mut decoder)
1090 .ok()?;
1091 Some(matrix.to_vec())
1092 });
1093
1094 let detected_rtc_offset = router.detect_rtc_offset_for_file(content, &mut decoder);
1099
1100 let frame = MeshFrame::select(site_transform.as_deref(), detected_rtc_offset);
1103 let coord_space = frame.coordinate_space();
1104 let has_rtc_offset = frame.needs_shift();
1105 router.set_rtc_offset(frame.rtc_offset());
1106 site_local::publish_baked_basis(baked_basis_out, frame, site_transform.as_deref());
1107 let preprocess_time = mark(Mark::PreprocessEnd, preprocess_start.elapsed());
1108 preprocess_span.record("phase_ms", preprocess_time.as_millis() as u64);
1109 drop(preprocess_span);
1110
1111 let parse_time = mark(Mark::ParseEnd, parse_start.elapsed());
1112 tracing::info!(
1113 entity_scan_time_ms = entity_scan_time.as_millis(),
1114 lookup_time_ms = lookup_time.as_millis(),
1115 preprocess_time_ms = preprocess_time.as_millis(),
1116 parse_time_ms = parse_time.as_millis(),
1117 "Parse phase complete, starting geometry extraction"
1118 );
1119
1120 let geometry_start = mark(Mark::GeometryStart, Clock::now());
1122 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1124 let rtc_offset = router.rtc_offset();
1125 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1133 let void_index_arc = Arc::new(filtered_void_index);
1134 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1135 let mut geometry_style_index = Arc::new(geometry_style_index);
1136 let indexed_colour_full = Arc::new(indexed_colour_full);
1137 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1142 content,
1143 &mut decoder,
1144 ));
1145 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1149 .iter()
1150 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1151 .collect();
1152 let element_material_colors = Arc::new(element_material_colors);
1153
1154 let total_jobs = entity_jobs.len();
1155 let initial_chunk_size = options.initial_batch_size.max(1);
1156 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1157 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1158 FxHashMap::default();
1159 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1160 FxHashMap::default();
1161 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1162 let mut meshes: Vec<MeshData> = Vec::new();
1163 let mut processed_jobs = 0usize;
1164 let mut total_meshes = 0usize;
1165 let mut total_vertices = 0usize;
1166 let mut total_triangles = 0usize;
1167 let mut chunk_start = 0usize;
1168 let mut current_chunk_size = initial_chunk_size;
1169
1170 let mut deferred_styles_applied = !defer_style_updates;
1171
1172 let diag_collectors = diagnostics::DiagnosticCollectors::new();
1175
1176 let item_dedup_cache = GeometryRouter::new_dedup_cache();
1181 let brep_signature_cache = GeometryRouter::new_brep_signature_cache();
1182
1183 let mapped_item_cache = GeometryRouter::new_mapped_item_cache();
1187
1188 let instancing_plan: Option<ifc_lite_geometry::MappedInstancePlan> = (options.enable_instancing
1218 && options.retain_emitted_meshes
1219 && coord_space != MeshCoordinateSpace::SiteLocal)
1220 .then(|| {
1221 Arc::new(
1222 mapped_item_plan
1223 .into_iter()
1224 .filter(|(_, (count, _))| *count >= 2)
1225 .collect::<FxHashMap<u32, (u32, u32)>>(),
1226 )
1227 });
1228 let indexed_colour_split_ids: Option<Arc<FxHashSet<u32>>> = (instancing_plan.is_some()
1247 && !indexed_colour_full.is_empty())
1248 .then(|| {
1249 let ids: FxHashSet<u32> = indexed_colour_full
1250 .iter()
1251 .filter(|(_, m)| m.has_multiple_colours())
1252 .map(|(&id, _)| id)
1253 .collect();
1254 (!ids.is_empty()).then(|| Arc::new(ids))
1255 })
1256 .flatten();
1257 let raw_instance_collector: std::sync::Mutex<Vec<RawInstanceOccurrence>> =
1260 std::sync::Mutex::new(Vec::new());
1261
1262 let point_cache_hits_collector = std::sync::atomic::AtomicU64::new(0);
1270 let point_cache_misses_collector = std::sync::atomic::AtomicU64::new(0);
1271 let faceted_brep_ns_collector = std::sync::atomic::AtomicU64::new(0);
1272
1273 let worker_point_caches = jobs::new_worker_point_caches();
1274 let worker_placement_caches = jobs::new_worker_placement_caches();
1278
1279 let geometry_span = tracing::info_span!(
1280 "geometry",
1281 element_count = total_jobs,
1282 mesh_count = tracing::field::Empty,
1283 triangle_count = tracing::field::Empty,
1284 backstop_count = tracing::field::Empty,
1285 total_csg_failures = tracing::field::Empty,
1286 phase_ms = tracing::field::Empty,
1287 );
1288 let geometry_guard = geometry_span.clone().entered();
1289
1290 while chunk_start < total_jobs {
1291 if options
1295 .cancel
1296 .as_ref()
1297 .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
1298 {
1299 break;
1300 }
1301 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1302 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1303
1304 #[cfg(not(target_arch = "wasm32"))]
1308 {
1309 let entity_index_for_meta = entity_index_arc.clone();
1311 jobs_chunk.par_iter_mut().for_each(|job| {
1312 if job.global_id.is_some()
1313 || job.name.is_some()
1314 || job.product_definition_shape_id.is_some()
1315 {
1316 return;
1317 }
1318 let local_decoder = entity_index_for_meta.decoder(content);
1319 let Ok(entity) = local_decoder.decode_at_uncached(job.start, job.end) else {
1321 return;
1322 };
1323 job.global_id = normalize_optional_string(entity.get_string(0));
1324 job.name = normalize_optional_string(entity.get_string(2));
1325 job.product_definition_shape_id = entity.get_ref(6);
1326 });
1327
1328 for job in jobs_chunk.iter_mut() {
1330 let Some(pds_id) = job.product_definition_shape_id else {
1331 continue;
1332 };
1333 let resolved_color = color_cache_by_product_definition_shape
1334 .entry(pds_id)
1335 .or_insert_with(|| {
1336 resolve_element_color_for_product_definition_shape(
1337 pds_id,
1338 &geometry_style_index,
1339 &mut decoder,
1340 )
1341 });
1342 if let Some(color) = resolved_color {
1343 job.element_color = *color;
1344 } else if let Some(color) = element_material_color.get(&job.id) {
1345 job.element_color = *color;
1348 }
1349 if options.include_presentation_layers {
1350 let resolved_layer = layer_cache_by_product_definition_shape
1351 .entry(pds_id)
1352 .or_insert_with(|| {
1353 resolve_presentation_layer_for_product_definition_shape(
1354 pds_id,
1355 &presentation_layer_by_assigned_id,
1356 &mut layer_cache_by_representation,
1357 &mut decoder,
1358 )
1359 });
1360 job.presentation_layer = resolved_layer.clone();
1361 }
1362 }
1363 }
1364
1365 #[cfg(target_arch = "wasm32")]
1367 for job in jobs_chunk.iter_mut() {
1368 populate_entity_job_metadata(
1369 job,
1370 &geometry_style_index,
1371 &element_material_color,
1372 &presentation_layer_by_assigned_id,
1373 &mut color_cache_by_product_definition_shape,
1374 &mut layer_cache_by_product_definition_shape,
1375 &mut layer_cache_by_representation,
1376 &mut decoder,
1377 options.include_presentation_layers,
1378 );
1379 }
1380 let site_local_rotation: Option<&Vec<f64>> =
1381 if coord_space == MeshCoordinateSpace::SiteLocal {
1382 site_transform.as_ref()
1383 } else {
1384 None
1385 };
1386 let chunk_meshes: Vec<MeshData> = jobs_chunk
1391 .par_iter()
1392 .map(|job| {
1393 let widx = rayon::current_thread_index().unwrap_or(0) % worker_point_caches.len();
1394 let mut fallback_cache = FxHashMap::default();
1404 let mut slot_guard = worker_point_caches[widx].try_lock().ok();
1405 let worker_point_cache: &mut FxHashMap<u32, (f64, f64, f64)> =
1406 match slot_guard.as_deref_mut() {
1407 Some(cache) => cache,
1408 None => &mut fallback_cache,
1409 };
1410 let mut fallback_placement_cache = FxHashMap::default();
1419 let mut placement_slot_guard = worker_placement_caches[widx].try_lock().ok();
1420 let worker_placement_cache: &mut FxHashMap<u32, [f64; 16]> =
1421 match placement_slot_guard.as_deref_mut() {
1422 Some(cache) => cache,
1423 None => &mut fallback_placement_cache,
1424 };
1425 process_entity_job(
1426 job,
1427 content,
1428 &entity_index_arc,
1429 unit_scale,
1430 rtc_offset,
1431 seed_plane_angle_to_radians,
1432 options.tessellation_quality,
1433 void_index_arc.as_ref(),
1434 skipped_entity_ids.as_ref(),
1435 geometry_style_index.as_ref(),
1436 indexed_colour_full.as_ref(),
1437 element_material_colors.as_ref(),
1438 texture_index.as_ref(),
1439 site_local_rotation,
1440 &diag_collectors,
1441 &item_dedup_cache,
1442 &brep_signature_cache,
1443 &mapped_item_cache,
1444 instancing_plan.as_ref(),
1445 indexed_colour_split_ids.as_ref(),
1446 &raw_instance_collector,
1447 worker_point_cache,
1448 worker_placement_cache,
1449 &point_cache_hits_collector,
1450 &point_cache_misses_collector,
1451 &faceted_brep_ns_collector,
1452 )
1453 })
1454 .flatten_iter()
1455 .collect();
1456
1457 processed_jobs += jobs_chunk.len();
1458 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1459 total_triangles += chunk_meshes
1460 .iter()
1461 .map(|m| m.triangle_count())
1462 .sum::<usize>();
1463
1464 if !chunk_meshes.is_empty() {
1465 total_meshes += chunk_meshes.len();
1466 let emit_mesh_chunk_size = current_chunk_size.max(1);
1467 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1468 on_batch(emitted_meshes, processed_jobs, total_jobs);
1469 }
1470 if options.retain_emitted_meshes {
1471 meshes.extend(chunk_meshes);
1472 }
1473
1474 if !deferred_styles_applied {
1475 let mut rebuilt_styles = {
1480 let mut style_decoder =
1481 entity_index_arc.decoder(content);
1482 crate::prepass::resolve_styled_item_spans(
1483 &deferred_styled_item_positions,
1484 &mut style_decoder,
1485 )
1486 };
1487 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1488 geometry_style_index = Arc::new(rebuilt_styles);
1489 let deferred_color_updates = build_color_updates_for_jobs(
1490 &entity_jobs[..processed_jobs],
1491 geometry_style_index.as_ref(),
1492 content,
1493 &entity_index_arc,
1494 );
1495 if !deferred_color_updates.is_empty() {
1496 on_color_update(&deferred_color_updates);
1497 }
1498 deferred_styles_applied = true;
1499 }
1500 }
1501 chunk_start = chunk_end;
1502 current_chunk_size = throughput_chunk_size;
1503 }
1504
1505 let geometry_time = mark(Mark::GeometryEnd, geometry_start.elapsed());
1506 let csg_failures = diag_collectors
1509 .csg_failures
1510 .into_inner()
1511 .unwrap_or_else(|poisoned| poisoned.into_inner());
1512 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1513 let products_with_failures = ifc_lite_geometry::count_attributed_products(&csg_failures);
1514 let backstop_dropped = diag_collectors.backstop.into_inner();
1515 let oversized_ref_drops = diag_collectors.oversized_ref_drops.into_inner();
1517 let point_cache_hits = point_cache_hits_collector.into_inner();
1518 let point_cache_misses = point_cache_misses_collector.into_inner();
1519 let faceted_brep_time_ms = faceted_brep_ns_collector.into_inner() / 1_000_000;
1520 geometry_span.record("mesh_count", total_meshes as u64);
1521 geometry_span.record("triangle_count", total_triangles as u64);
1522 geometry_span.record("backstop_count", backstop_dropped);
1523 geometry_span.record("total_csg_failures", total_csg_failures as u64);
1524 geometry_span.record("phase_ms", geometry_time.as_millis() as u64);
1525 drop(geometry_guard);
1526 if total_csg_failures > 0 {
1527 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1528 for fails in csg_failures.values() {
1529 for f in fails {
1530 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1531 }
1532 }
1533 let open_topology_accepted = *by_reason.get("KernelError").unwrap_or(&0);
1538 let dropped = total_csg_failures - open_topology_accepted;
1539 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1540 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1541 let breakdown = breakdown
1542 .iter()
1543 .map(|(reason, count)| format!("{reason}={count}"))
1544 .collect::<Vec<_>>()
1545 .join(" ");
1546 tracing::warn!(
1547 total_csg_failures,
1548 products_with_failures,
1549 dropped,
1550 open_topology_accepted,
1551 %breakdown,
1552 "{}",
1553 csg_summary::csg_summary_message(dropped, open_topology_accepted)
1554 );
1555 }
1556
1557 let geometry_diagnostics = tracing::debug_span!("collate_diagnostics").in_scope(|| {
1558 diagnostics::collate(
1559 diag_collectors.classification,
1560 diag_collectors.host_diags,
1561 diag_collectors.rect_fast,
1562 diag_collectors.unsupported_items,
1563 &csg_failures,
1564 oversized_ref_drops,
1565 )
1566 });
1567
1568 let instances = instancing::finalize_instances(
1573 raw_instance_collector
1574 .into_inner()
1575 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1576 &mut meshes,
1577 &mapped_item_cache,
1578 [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1579 );
1580
1581 let total_time = mark(Mark::TotalEnd, total_start.elapsed());
1582 pipeline_span.record("element_count", total_jobs as u64);
1583 pipeline_span.record("total_ms", total_time.as_millis() as u64);
1584
1585 tracing::info!(
1586 meshes = meshes.len(),
1587 instances = instances.len(),
1588 vertices = total_vertices,
1589 triangles = total_triangles,
1590 backstop_count = backstop_dropped,
1591 geometry_time_ms = geometry_time.as_millis(),
1592 total_time_ms = total_time.as_millis(),
1593 "Geometry processing complete"
1594 );
1595
1596 let extract_georeferencing = || crate::georeferencing::extract_georeferencing_from_candidates(
1597 &mut entity_index_arc.decoder(content), &georeferencing_candidates,
1598 );
1599 #[cfg(not(target_arch = "wasm32"))]
1603 let georeferencing = rayon::in_place_scope(|scope| {
1604 scope.spawn(move |_| drop(decoder));
1605 scope.spawn(move |_| drop(item_dedup_cache));
1606 extract_georeferencing()
1607 });
1608 #[cfg(target_arch = "wasm32")]
1609 let georeferencing = extract_georeferencing();
1610
1611 ProcessingResult {
1612 meshes,
1613 instances,
1614 mesh_coordinate_space: coord_space,
1615 frame,
1616 site_transform,
1617 building_transform,
1618 metadata: ModelMetadata {
1619 schema_version,
1620 entity_count: total_entities,
1621 geometry_entity_count,
1622 coordinate_info: CoordinateInfo {
1623 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1624 is_geo_referenced: has_rtc_offset,
1625 },
1626 length_unit_scale: Some(unit_scale),
1627 georeferencing,
1628 },
1629 stats: ProcessingStats {
1630 total_meshes,
1631 total_vertices,
1632 total_triangles,
1633 parse_time_ms: parse_time.as_millis() as u64,
1634 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1635 lookup_time_ms: lookup_time.as_millis() as u64,
1636 preprocess_time_ms: preprocess_time.as_millis() as u64,
1637 geometry_time_ms: geometry_time.as_millis() as u64,
1638 total_time_ms: total_time.as_millis() as u64,
1639 from_cache: false,
1640 total_csg_failures: total_csg_failures as u64,
1641 products_with_failures,
1642 degenerate_triangles_dropped: backstop_dropped,
1643 point_cache_hits,
1644 point_cache_misses,
1645 faceted_brep_time_ms,
1646 geometry_diagnostics,
1647 },
1648 }
1649}
1650
1651