1use crate::types::mesh::MeshData;
10use crate::types::response::{
11 CoordinateInfo, ModelMetadata, ProcessingStats, QuickMetadataBootstrap,
12 QuickMetadataEntitySummary, QuickMetadataSpatialNode,
13};
14use ifc_lite_core::{
15 build_entity_index, AttributeValue, 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
25#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum OpeningFilterMode {
29 #[default]
31 Default = 0,
32 IgnoreAll = 1,
34 IgnoreOpaque = 2,
36}
37
38impl OpeningFilterMode {
39 pub fn cache_key_suffix(&self) -> &'static str {
42 match self {
43 Self::Default => "default",
44 Self::IgnoreAll => "ignore_all",
45 Self::IgnoreOpaque => "ignore_opaque",
46 }
47 }
48}
49
50pub struct ProcessingResult {
52 pub meshes: Vec<MeshData>,
53 pub mesh_coordinate_space: Option<String>,
55 pub site_transform: Option<Vec<f64>>,
57 pub building_transform: Option<Vec<f64>>,
59 pub metadata: ModelMetadata,
60 pub stats: ProcessingStats,
61}
62
63#[derive(Debug, Clone, Copy)]
65pub struct StreamingOptions {
66 pub initial_batch_size: usize,
68 pub throughput_batch_size: usize,
70 pub fast_first_batch: bool,
72 pub include_properties: bool,
74 pub include_presentation_layers: bool,
76 pub emit_quick_metadata_bootstrap: bool,
78 pub retain_emitted_meshes: bool,
80 pub tessellation_quality: TessellationQuality,
87}
88
89impl Default for StreamingOptions {
90 fn default() -> Self {
91 Self {
92 initial_batch_size: 50,
93 throughput_batch_size: 50,
94 fast_first_batch: false,
95 include_properties: true,
96 include_presentation_layers: true,
97 emit_quick_metadata_bootstrap: false,
98 retain_emitted_meshes: true,
99 tessellation_quality: TessellationQuality::default(),
100 }
101 }
102}
103
104const SITE_LOCAL_MESH_COORDINATE_SPACE: &str = "site_local";
105const MODEL_RTC_MESH_COORDINATE_SPACE: &str = "model_rtc";
106const RAW_IFC_MESH_COORDINATE_SPACE: &str = "raw_ifc";
107
108const PLACEMENT_IDENTITY_EPSILON: f64 = 1e-9;
112
113#[inline]
114fn translation_is_nonidentity(t: (f64, f64, f64)) -> bool {
115 t.0.abs() > PLACEMENT_IDENTITY_EPSILON
116 || t.1.abs() > PLACEMENT_IDENTITY_EPSILON
117 || t.2.abs() > PLACEMENT_IDENTITY_EPSILON
118}
119
120fn apply_inverse_rotation_in_place(values: &mut [f32], column_major_matrix: &[f64]) {
125 if values.len() < 3 || column_major_matrix.len() < 16 {
126 return;
127 }
128
129 let r00 = column_major_matrix[0];
130 let r10 = column_major_matrix[1];
131 let r20 = column_major_matrix[2];
132 let r01 = column_major_matrix[4];
133 let r11 = column_major_matrix[5];
134 let r21 = column_major_matrix[6];
135 let r02 = column_major_matrix[8];
136 let r12 = column_major_matrix[9];
137 let r22 = column_major_matrix[10];
138
139 let is_identity = (r00 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
140 && r10.abs() < PLACEMENT_IDENTITY_EPSILON
141 && r20.abs() < PLACEMENT_IDENTITY_EPSILON
142 && r01.abs() < PLACEMENT_IDENTITY_EPSILON
143 && (r11 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
144 && r21.abs() < PLACEMENT_IDENTITY_EPSILON
145 && r02.abs() < PLACEMENT_IDENTITY_EPSILON
146 && r12.abs() < PLACEMENT_IDENTITY_EPSILON
147 && (r22 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON;
148 if is_identity {
149 return;
150 }
151
152 for chunk in values.chunks_exact_mut(3) {
153 let x = chunk[0] as f64;
154 let y = chunk[1] as f64;
155 let z = chunk[2] as f64;
156 chunk[0] = (r00 * x + r10 * y + r20 * z) as f32;
157 chunk[1] = (r01 * x + r11 * y + r21 * z) as f32;
158 chunk[2] = (r02 * x + r12 * y + r22 * z) as f32;
159 }
160}
161
162pub fn convert_mesh_to_site_local(mesh: &mut MeshData, site_transform: Option<&Vec<f64>>) {
169 let Some(site_transform) = site_transform else {
170 return;
171 };
172
173 apply_inverse_rotation_in_place(&mut mesh.positions, site_transform);
174 apply_inverse_rotation_in_place(&mut mesh.normals, site_transform);
175 apply_inverse_rotation_point_f64(&mut mesh.origin, site_transform);
180}
181
182fn apply_inverse_rotation_point_f64(p: &mut [f64; 3], column_major_matrix: &[f64]) {
185 if column_major_matrix.len() < 16 || (p[0] == 0.0 && p[1] == 0.0 && p[2] == 0.0) {
186 return;
187 }
188 let (r00, r10, r20) = (
189 column_major_matrix[0],
190 column_major_matrix[1],
191 column_major_matrix[2],
192 );
193 let (r01, r11, r21) = (
194 column_major_matrix[4],
195 column_major_matrix[5],
196 column_major_matrix[6],
197 );
198 let (r02, r12, r22) = (
199 column_major_matrix[8],
200 column_major_matrix[9],
201 column_major_matrix[10],
202 );
203 let (x, y, z) = (p[0], p[1], p[2]);
204 p[0] = r00 * x + r10 * y + r20 * z;
205 p[1] = r01 * x + r11 * y + r21 * z;
206 p[2] = r02 * x + r12 * y + r22 * z;
207}
208
209struct EntityJob {
211 id: u32,
212 ifc_type: IfcType,
213 start: usize,
214 end: usize,
215 product_definition_shape_id: Option<u32>,
216 element_color: [f32; 4],
217 global_id: Option<String>,
218 name: Option<String>,
219 presentation_layer: Option<String>,
220 space_zone_properties: Option<BTreeMap<String, String>>,
221 representation_map_id: Option<u32>,
225}
226
227fn populate_entity_job_metadata(
228 job: &mut EntityJob,
229 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
230 element_material_color: &FxHashMap<u32, [f32; 4]>,
231 layer_by_assigned_representation: &FxHashMap<u32, String>,
232 color_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<[f32; 4]>>,
233 layer_cache_by_product_definition_shape: &mut FxHashMap<u32, Option<String>>,
234 layer_cache_by_representation: &mut FxHashMap<u32, Option<String>>,
235 decoder: &mut EntityDecoder,
236 include_presentation_layers: bool,
237) {
238 if job.global_id.is_some() || job.name.is_some() || job.product_definition_shape_id.is_some() {
239 return;
240 }
241
242 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
243 return;
244 };
245
246 job.global_id = normalize_optional_string(entity.get_string(0));
247 job.name = normalize_optional_string(entity.get_string(2));
248 job.product_definition_shape_id = entity.get_ref(6);
249
250 let Some(product_definition_shape_id) = job.product_definition_shape_id else {
251 return;
252 };
253
254 let resolved_color = color_cache_by_product_definition_shape
255 .entry(product_definition_shape_id)
256 .or_insert_with(|| {
257 resolve_element_color_for_product_definition_shape(
258 product_definition_shape_id,
259 geometry_style_index,
260 decoder,
261 )
262 });
263 if let Some(color) = resolved_color {
264 job.element_color = *color;
265 } else if let Some(color) = element_material_color.get(&job.id) {
266 job.element_color = *color;
267 }
268
269 if include_presentation_layers {
270 let resolved_layer = layer_cache_by_product_definition_shape
271 .entry(product_definition_shape_id)
272 .or_insert_with(|| {
273 resolve_presentation_layer_for_product_definition_shape(
274 product_definition_shape_id,
275 layer_by_assigned_representation,
276 layer_cache_by_representation,
277 decoder,
278 )
279 });
280 job.presentation_layer = resolved_layer.clone();
281 }
282}
283
284use crate::style::GeometryStyleInfo;
288
289#[derive(Debug, Clone)]
290struct PropertySetDefinition {
291 name: Option<String>,
292 property_ids: Vec<u32>,
293}
294
295#[derive(Debug, Clone)]
296struct RelDefinesByPropertiesLink {
297 property_set_id: u32,
298 related_object_ids: Vec<u32>,
299}
300
301pub(crate) fn get_refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
303 let list = entity.get_list(index)?;
304 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
305 if refs.is_empty() {
306 None
307 } else {
308 Some(refs)
309 }
310}
311
312fn normalize_optional_string(raw: Option<&str>) -> Option<String> {
313 let value = raw?.trim();
314 if value.is_empty() || value == "$" {
315 return None;
316 }
317 Some(value.to_string())
318}
319
320fn normalize_ifc_property_name(raw: Option<&str>) -> Option<String> {
321 let name = normalize_optional_string(raw)?;
322 let cleaned = name.trim();
323 if cleaned.is_empty() {
324 return None;
325 }
326
327 Some(cleaned.to_string())
328}
329
330fn is_space_or_zone_type(ifc_type: &IfcType) -> bool {
331 matches!(
332 ifc_type,
333 IfcType::IfcSpace
334 | IfcType::IfcSpaceType
335 | IfcType::IfcZone
336 | IfcType::IfcSpatialZone
337 | IfcType::IfcSpatialZoneType
338 )
339}
340
341fn collect_property_set_definition(property_set: &DecodedEntity) -> Option<PropertySetDefinition> {
342 let property_ids = property_set
343 .get_list(4)
344 .or_else(|| property_set.get_list(2))
345 .map(|items| {
346 items
347 .iter()
348 .filter_map(AttributeValue::as_entity_ref)
349 .collect::<Vec<u32>>()
350 })
351 .unwrap_or_default();
352
353 if property_ids.is_empty() {
354 return None;
355 }
356
357 let name = normalize_optional_string(property_set.get_string(2))
358 .or_else(|| normalize_optional_string(property_set.get_string(0)));
359
360 Some(PropertySetDefinition { name, property_ids })
361}
362
363fn collect_rel_defines_by_properties_link(
364 rel_defines: &DecodedEntity,
365) -> Option<RelDefinesByPropertiesLink> {
366 let property_set_id = rel_defines.get_ref(5).or_else(|| rel_defines.get_ref(3))?;
367 let related_object_ids = rel_defines
368 .get_list(4)
369 .or_else(|| rel_defines.get_list(2))
370 .map(|items| {
371 items
372 .iter()
373 .filter_map(AttributeValue::as_entity_ref)
374 .collect::<Vec<u32>>()
375 })
376 .unwrap_or_default();
377
378 if related_object_ids.is_empty() {
379 return None;
380 }
381
382 Some(RelDefinesByPropertiesLink {
383 property_set_id,
384 related_object_ids,
385 })
386}
387
388fn attribute_list_to_string(values: &[AttributeValue]) -> Option<String> {
389 let tokens = values
390 .iter()
391 .filter_map(attribute_value_to_string)
392 .collect::<Vec<String>>();
393
394 if tokens.is_empty() {
395 return None;
396 }
397
398 Some(tokens.join("; "))
399}
400
401fn attribute_value_to_string(value: &AttributeValue) -> Option<String> {
402 match value {
403 AttributeValue::Null | AttributeValue::Derived => None,
404 AttributeValue::String(text) => normalize_optional_string(Some(text)),
405 AttributeValue::Enum(text) => normalize_optional_string(Some(text.trim_matches('.'))),
406 AttributeValue::Integer(number) => Some(number.to_string()),
407 AttributeValue::Float(number) => Some(number.to_string()),
408 AttributeValue::EntityRef(id) => Some(format!("#{id}")),
409 AttributeValue::List(values) => {
410 if values.len() >= 2 && matches!(values.first(), Some(AttributeValue::String(_))) {
411 return values.get(1).and_then(attribute_value_to_string);
412 }
413
414 attribute_list_to_string(values)
415 }
416 }
417}
418
419fn extract_property_name_and_value(property_entity: &DecodedEntity) -> Option<(String, String)> {
420 let property_name = normalize_ifc_property_name(property_entity.get_string(0))
421 .or_else(|| normalize_ifc_property_name(property_entity.get_string(2)))?;
422
423 let property_type = property_entity.ifc_type.name();
424 let value = match property_type {
425 "IfcPropertySingleValue" => property_entity.get(2).and_then(attribute_value_to_string),
426 "IfcPropertyEnumeratedValue" => property_entity.get(2).and_then(attribute_value_to_string),
427 "IfcPropertyListValue" => property_entity.get(2).and_then(attribute_value_to_string),
428 "IfcPropertyBoundedValue" => {
429 let lower = property_entity.get(2).and_then(attribute_value_to_string);
430 let upper = property_entity.get(3).and_then(attribute_value_to_string);
431 match (lower, upper) {
432 (Some(lo), Some(hi)) => Some(format!("{lo}..{hi}")),
433 (Some(lo), None) => Some(lo),
434 (None, Some(hi)) => Some(hi),
435 (None, None) => None,
436 }
437 }
438 "IfcPropertyReferenceValue" => property_entity.get(2).and_then(attribute_value_to_string),
439 _ => None,
440 }?;
441
442 let normalized_value = value.trim();
443 if normalized_value.is_empty() || normalized_value == "$" {
444 return None;
445 }
446
447 Some((property_name, normalized_value.to_string()))
448}
449
450fn add_space_zone_property(
451 attributes: &mut BTreeMap<String, String>,
452 property_set_name: Option<&str>,
453 property_name: &str,
454 property_value: &str,
455) {
456 if property_name.trim().is_empty() || property_value.trim().is_empty() {
457 return;
458 }
459
460 attributes
461 .entry(property_name.to_string())
462 .or_insert_with(|| property_value.to_string());
463
464 if let Some(pset_name) = normalize_optional_string(property_set_name) {
465 let scoped_name = format!("{}.{}", pset_name, property_name);
466 attributes
467 .entry(scoped_name)
468 .or_insert_with(|| property_value.to_string());
469 }
470}
471
472fn build_space_zone_properties_by_entity(
473 entity_jobs: &[EntityJob],
474 property_values_by_id: &FxHashMap<u32, (String, String)>,
475 property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
476 rel_defines_by_properties: &[RelDefinesByPropertiesLink],
477) -> FxHashMap<u32, BTreeMap<String, String>> {
478 let mut target_space_zone_ids = FxHashMap::default();
479 for job in entity_jobs
480 .iter()
481 .filter(|job| is_space_or_zone_type(&job.ifc_type))
482 {
483 target_space_zone_ids.insert(job.id, ());
484 }
485
486 if target_space_zone_ids.is_empty() {
487 return FxHashMap::default();
488 }
489
490 let mut properties_by_entity: FxHashMap<u32, BTreeMap<String, String>> = FxHashMap::default();
491
492 for link in rel_defines_by_properties {
493 let Some(property_set) = property_sets_by_id.get(&link.property_set_id) else {
494 continue;
495 };
496
497 for related_id in &link.related_object_ids {
498 if !target_space_zone_ids.contains_key(related_id) {
499 continue;
500 }
501
502 let attributes = properties_by_entity.entry(*related_id).or_default();
503 for property_id in &property_set.property_ids {
504 let Some((property_name, property_value)) = property_values_by_id.get(property_id)
505 else {
506 continue;
507 };
508
509 add_space_zone_property(
510 attributes,
511 property_set.name.as_deref(),
512 property_name,
513 property_value,
514 );
515 }
516 }
517 }
518
519 properties_by_entity
520}
521
522fn assign_space_zone_properties(
523 entity_jobs: &mut [EntityJob],
524 property_values_by_id: &FxHashMap<u32, (String, String)>,
525 property_sets_by_id: &FxHashMap<u32, PropertySetDefinition>,
526 rel_defines_by_properties: &[RelDefinesByPropertiesLink],
527) {
528 let properties_by_entity = build_space_zone_properties_by_entity(
529 entity_jobs,
530 property_values_by_id,
531 property_sets_by_id,
532 rel_defines_by_properties,
533 );
534
535 if properties_by_entity.is_empty() {
536 return;
537 }
538
539 for job in entity_jobs.iter_mut() {
540 if let Some(properties) = properties_by_entity.get(&job.id) {
541 job.space_zone_properties = Some(properties.clone());
542 }
543 }
544}
545
546#[derive(Clone)]
547struct QuickSpatialNodeEntry {
548 express_id: u32,
549 type_name: String,
550 name: String,
551 elevation: Option<f64>,
552 children: Vec<u32>,
553 elements: Vec<u32>,
554 parent: Option<u32>,
555}
556
557#[inline]
559fn is_quick_spatial_type_ci(type_name: &str) -> bool {
560 type_name.eq_ignore_ascii_case("IFCPROJECT")
561 || type_name.eq_ignore_ascii_case("IFCSITE")
562 || type_name.eq_ignore_ascii_case("IFCBUILDING")
563 || type_name.eq_ignore_ascii_case("IFCBUILDINGSTOREY")
564 || type_name.eq_ignore_ascii_case("IFCSPACE")
565 || type_name.eq_ignore_ascii_case("IFCSPATIALZONE")
566 || type_name.eq_ignore_ascii_case("IFCFACILITY")
567 || type_name.eq_ignore_ascii_case("IFCFACILITYPART")
568 || type_name.eq_ignore_ascii_case("IFCBRIDGE")
569 || type_name.eq_ignore_ascii_case("IFCBRIDGEPART")
570 || type_name.eq_ignore_ascii_case("IFCROAD")
571 || type_name.eq_ignore_ascii_case("IFCROADPART")
572 || type_name.eq_ignore_ascii_case("IFCRAILWAY")
573 || type_name.eq_ignore_ascii_case("IFCRAILWAYPART")
574}
575
576fn parse_step_arguments(entity_bytes: &[u8]) -> Vec<&[u8]> {
577 let Some(open_idx) = entity_bytes.iter().position(|byte| *byte == b'(') else {
578 return Vec::new();
579 };
580 let Some(close_idx) = entity_bytes.iter().rposition(|byte| *byte == b')') else {
581 return Vec::new();
582 };
583 if close_idx <= open_idx {
584 return Vec::new();
585 }
586 let args = &entity_bytes[open_idx + 1..close_idx];
587 let mut parts = Vec::new();
588 let mut in_string = false;
589 let mut depth = 0i32;
590 let mut start = 0usize;
591 let bytes = args;
592 let mut index = 0usize;
593 while index < bytes.len() {
594 match bytes[index] {
595 b'\'' => {
596 if in_string && index + 1 < bytes.len() && bytes[index + 1] == b'\'' {
597 index += 1;
598 } else {
599 in_string = !in_string;
600 }
601 }
602 b'(' if !in_string => depth += 1,
603 b')' if !in_string => depth -= 1,
604 b',' if !in_string && depth == 0 => {
605 parts.push(args[start..index].trim_ascii());
606 start = index + 1;
607 }
608 _ => {}
609 }
610 index += 1;
611 }
612 if start <= args.len() {
613 parts.push(args[start..].trim_ascii());
614 }
615 parts
616}
617
618fn parse_step_string(token: &[u8]) -> Option<String> {
619 let trimmed = token.trim_ascii();
620 if trimmed.len() < 2 || trimmed[0] != b'\'' || trimmed[trimmed.len() - 1] != b'\'' {
621 return None;
622 }
623 Some(String::from_utf8_lossy(&trimmed[1..trimmed.len() - 1]).replace("''", "'"))
624}
625
626fn parse_step_ref(token: &[u8]) -> Option<u32> {
627 std::str::from_utf8(token.trim_ascii().strip_prefix(b"#")?)
628 .ok()?
629 .parse()
630 .ok()
631}
632
633fn parse_step_ref_list(token: &[u8]) -> Vec<u32> {
634 let trimmed = token.trim_ascii();
635 let inner = trimmed
636 .strip_prefix(b"(")
637 .and_then(|value| value.strip_suffix(b")"))
638 .unwrap_or(trimmed);
639 inner.split(|byte| *byte == b',').filter_map(parse_step_ref).collect()
640}
641
642fn extract_name_from_args(args: &[&[u8]], fallback: &str) -> String {
643 args.get(2)
644 .and_then(|token| parse_step_string(token))
645 .filter(|value| !value.trim().is_empty())
646 .unwrap_or_else(|| fallback.to_string())
647}
648
649fn extract_storey_elevation_from_args(args: &[&[u8]]) -> Option<f64> {
650 for index in [9usize, 8usize] {
651 if let Some(value) = args
652 .get(index)
653 .and_then(|token| std::str::from_utf8(token.trim_ascii()).ok())
654 .and_then(|token| token.parse::<f64>().ok())
655 {
656 return Some(value);
657 }
658 }
659 args.iter()
660 .filter_map(|token| std::str::from_utf8(token.trim_ascii()).ok())
661 .filter_map(|token| token.parse::<f64>().ok())
662 .find(|value| value.abs() < 10_000.0)
663}
664
665fn build_quick_spatial_tree_node(
666 express_id: u32,
667 nodes: &HashMap<u32, QuickSpatialNodeEntry>,
668 element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
669) -> Result<QuickMetadataSpatialNode, String> {
670 let node = nodes
671 .get(&express_id)
672 .ok_or_else(|| format!("Quick spatial node #{express_id} not found"))?;
673 let mut children = Vec::with_capacity(node.children.len());
674 for child_id in &node.children {
675 children.push(build_quick_spatial_tree_node(
676 *child_id,
677 nodes,
678 element_summaries,
679 )?);
680 }
681 let elements = node
682 .elements
683 .iter()
684 .map(|element_id| {
685 element_summaries
686 .get(element_id)
687 .cloned()
688 .unwrap_or(QuickMetadataEntitySummary {
689 express_id: *element_id,
690 type_name: "IfcProduct".to_string(),
691 name: format!("IfcProduct #{}", element_id),
692 global_id: None,
693 kind: "element".to_string(),
694 has_children: false,
695 element_count: None,
696 elevation: None,
697 })
698 })
699 .collect();
700 Ok(QuickMetadataSpatialNode {
701 summary: QuickMetadataEntitySummary {
702 express_id: node.express_id,
703 type_name: node.type_name.clone(),
704 name: node.name.clone(),
705 global_id: None,
706 kind: "spatial".to_string(),
707 has_children: !node.children.is_empty() || !node.elements.is_empty(),
708 element_count: Some(node.elements.len()),
709 elevation: node.elevation,
710 },
711 children,
712 elements,
713 })
714}
715
716fn geometry_priority_score(ifc_type: &IfcType) -> u8 {
717 match ifc_type {
718 IfcType::IfcWall | IfcType::IfcWallStandardCase => 100,
719 IfcType::IfcSlab => 95,
720 IfcType::IfcColumn => 90,
721 IfcType::IfcBeam => 85,
722 IfcType::IfcRoof => 80,
723 IfcType::IfcStair | IfcType::IfcStairFlight => 75,
724 IfcType::IfcCurtainWall => 70,
725 IfcType::IfcFooting | IfcType::IfcPile => 65,
726 IfcType::IfcDoor | IfcType::IfcWindow => 30,
727 IfcType::IfcFurnishingElement => 10,
728 _ => 50,
729 }
730}
731
732pub fn process_geometry<T>(content: &T) -> ProcessingResult
734where
735 T: AsRef<[u8]> + ?Sized,
736{
737 process_geometry_filtered(content.as_ref(), OpeningFilterMode::Default)
738}
739
740pub fn process_geometry_streaming(
742 content: &[u8],
743 batch_size: usize,
744 on_batch: impl FnMut(&[MeshData], usize, usize),
745) -> ProcessingResult {
746 process_geometry_streaming_with_options(
747 content,
748 StreamingOptions {
749 initial_batch_size: batch_size,
750 throughput_batch_size: batch_size,
751 ..StreamingOptions::default()
752 },
753 on_batch,
754 |_| {},
755 )
756}
757
758pub fn process_geometry_streaming_with_options(
760 content: &[u8],
761 options: StreamingOptions,
762 on_batch: impl FnMut(&[MeshData], usize, usize),
763 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
764) -> ProcessingResult {
765 process_geometry_streaming_with_options_and_bootstrap(
766 content,
767 options,
768 on_batch,
769 on_color_update,
770 |_| {},
771 )
772}
773
774pub fn process_geometry_streaming_with_options_and_bootstrap(
777 content: &[u8],
778 options: StreamingOptions,
779 on_batch: impl FnMut(&[MeshData], usize, usize),
780 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
781 on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
782) -> ProcessingResult {
783 process_geometry_streaming_filtered_with_options(
784 content,
785 OpeningFilterMode::Default,
786 options,
787 on_batch,
788 on_color_update,
789 on_quick_metadata_bootstrap,
790 )
791}
792
793pub fn process_geometry_filtered<T>(
795 content: &T,
796 opening_filter: OpeningFilterMode,
797) -> ProcessingResult
798where
799 T: AsRef<[u8]> + ?Sized,
800{
801 process_geometry_filtered_with_quality(content, opening_filter, TessellationQuality::default())
802}
803
804pub fn process_geometry_filtered_with_quality<T>(
808 content: &T,
809 opening_filter: OpeningFilterMode,
810 tessellation_quality: TessellationQuality,
811) -> ProcessingResult
812where
813 T: AsRef<[u8]> + ?Sized,
814{
815 let content = content.as_ref();
816 process_geometry_streaming_filtered_with_options(
817 content,
818 opening_filter,
819 StreamingOptions {
820 initial_batch_size: usize::MAX,
821 throughput_batch_size: usize::MAX,
822 tessellation_quality,
823 ..StreamingOptions::default()
824 },
825 |_, _, _| {},
826 |_| {},
827 |_| {},
828 )
829}
830
831pub fn process_geometry_streaming_filtered(
833 content: &[u8],
834 opening_filter: OpeningFilterMode,
835 batch_size: usize,
836 on_batch: impl FnMut(&[MeshData], usize, usize),
837 on_color_update: impl FnMut(&[(u32, [f32; 4])]),
838) -> ProcessingResult {
839 process_geometry_streaming_filtered_with_options(
840 content,
841 opening_filter,
842 StreamingOptions {
843 initial_batch_size: batch_size,
844 throughput_batch_size: batch_size,
845 ..StreamingOptions::default()
846 },
847 on_batch,
848 on_color_update,
849 |_| {},
850 )
851}
852
853pub fn process_geometry_streaming_filtered_with_options(
855 content: &[u8],
856 opening_filter: OpeningFilterMode,
857 options: StreamingOptions,
858 mut on_batch: impl FnMut(&[MeshData], usize, usize),
859 mut on_color_update: impl FnMut(&[(u32, [f32; 4])]),
860 mut on_quick_metadata_bootstrap: impl FnMut(&QuickMetadataBootstrap),
861) -> ProcessingResult {
862 let total_start = std::time::Instant::now();
863 let parse_start = std::time::Instant::now();
864 let entity_scan_start = std::time::Instant::now();
865
866 tracing::info!(
867 content_size = content.len(),
868 "Starting IFC geometry processing"
869 );
870
871 let entity_index = Arc::new(build_entity_index(content));
873 let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
874 tracing::debug!("Built entity index");
875
876 let mut prepass_spans = crate::prepass::PrepassSpans::default();
882 let mut project_id: Option<u32> = None;
883 let mut presentation_layer_by_assigned_id: FxHashMap<u32, String> = FxHashMap::default();
884 let mut property_values_by_id: FxHashMap<u32, (String, String)> = FxHashMap::default();
885 let mut property_sets_by_id: FxHashMap<u32, PropertySetDefinition> = FxHashMap::default();
886 let mut rel_defines_by_properties: Vec<RelDefinesByPropertiesLink> = Vec::new();
887
888 let mut scanner = EntityScanner::new(content);
890 let mut entity_jobs: Vec<EntityJob> = Vec::with_capacity(2000);
891 let mut type_product_geometry: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
896 let mut referenced_representation_maps: FxHashSet<u32> = FxHashSet::default();
897 let mut instantiated_type_ids: FxHashSet<u32> = FxHashSet::default();
906 let quick_metadata_enabled = options.emit_quick_metadata_bootstrap;
907 let mut quick_spatial_nodes =
908 quick_metadata_enabled.then(HashMap::<u32, QuickSpatialNodeEntry>::new);
909 let mut quick_aggregate_links = if quick_metadata_enabled {
910 Vec::<(u32, Vec<u32>)>::new()
911 } else {
912 Vec::new()
913 };
914 let mut quick_containment_links = if quick_metadata_enabled {
915 Vec::<(u32, Vec<u32>)>::new()
916 } else {
917 Vec::new()
918 };
919 let mut quick_referenced_links = if quick_metadata_enabled {
924 Vec::<(u32, Vec<u32>)>::new()
925 } else {
926 Vec::new()
927 };
928 let mut quick_element_summaries = if quick_metadata_enabled {
929 HashMap::<u32, QuickMetadataEntitySummary>::new()
930 } else {
931 HashMap::new()
932 };
933 let mut schema_version = "IFC2X3".to_string();
934 let mut total_entities = 0usize;
935 let mut site_entity_pos: Option<(usize, usize)> = None;
936 let mut building_entity_pos: Option<(usize, usize)> = None;
937
938 let defer_style_updates = options.fast_first_batch
939 && opening_filter == OpeningFilterMode::Default
940 && !options.include_presentation_layers;
941
942 while let Some((id, type_name, start, end)) = scanner.next_entity() {
943 total_entities += 1;
944 if let Some(spatial_nodes) = quick_spatial_nodes.as_mut() {
945 if is_quick_spatial_type_ci(type_name) {
947 let args = parse_step_arguments(&content[start..end]);
948 let fallback = format!("{type_name} #{id}");
949 spatial_nodes.entry(id).or_insert(QuickSpatialNodeEntry {
950 express_id: id,
951 type_name: type_name.to_string(),
952 name: extract_name_from_args(&args, &fallback),
953 elevation: if type_name.eq_ignore_ascii_case("IfcBuildingStorey") {
954 extract_storey_elevation_from_args(&args)
955 } else {
956 None
957 },
958 children: Vec::new(),
959 elements: Vec::new(),
960 parent: None,
961 });
962 } else if type_name.eq_ignore_ascii_case("IFCRELAGGREGATES") {
963 let args = parse_step_arguments(&content[start..end]);
964 if let Some(parent_id) = args.get(4).and_then(|token| parse_step_ref(token)) {
965 quick_aggregate_links.push((
966 parent_id,
967 args.get(5)
968 .map(|token| parse_step_ref_list(token))
969 .unwrap_or_default(),
970 ));
971 }
972 } else if type_name.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
973 let args = parse_step_arguments(&content[start..end]);
974 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
975 quick_containment_links.push((
976 parent_id,
977 args.get(4)
978 .map(|token| parse_step_ref_list(token))
979 .unwrap_or_default(),
980 ));
981 }
982 } else if type_name.eq_ignore_ascii_case("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
983 let args = parse_step_arguments(&content[start..end]);
984 if let Some(parent_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
985 quick_referenced_links.push((
986 parent_id,
987 args.get(4)
988 .map(|token| parse_step_ref_list(token))
989 .unwrap_or_default(),
990 ));
991 }
992 }
993 }
994
995 if type_name == "IFCINDEXEDCOLOURMAP" {
996 prepass_spans.indexed_colour_maps.push((id, start, end));
998 continue;
999 }
1000
1001 if type_name == "IFCSTYLEDITEM" {
1002 prepass_spans.styled_items.push((id, start, end));
1006 continue;
1007 } else if type_name == "IFCMATERIALDEFINITIONREPRESENTATION" {
1008 prepass_spans.material_def_reprs.push((id, start, end));
1009 continue;
1010 } else if type_name == "IFCRELASSOCIATESMATERIAL" {
1011 prepass_spans.rel_associates_material.push((id, start, end));
1012 continue;
1013 } else if type_name == "IFCPRESENTATIONLAYERASSIGNMENT" {
1014 if !options.include_presentation_layers {
1015 continue;
1016 }
1017 if let Ok(layer_assignment) = decoder.decode_at(start, end) {
1018 collect_presentation_layer_assignments(
1019 &mut presentation_layer_by_assigned_id,
1020 &layer_assignment,
1021 );
1022 }
1023 continue;
1024 } else if type_name == "IFCPROPERTYSET" {
1025 if !options.include_properties {
1026 continue;
1027 }
1028 if let Ok(property_set) = decoder.decode_at(start, end) {
1029 if let Some(definition) = collect_property_set_definition(&property_set) {
1030 property_sets_by_id.insert(id, definition);
1031 }
1032 }
1033 continue;
1034 } else if type_name == "IFCRELDEFINESBYPROPERTIES" {
1035 if !options.include_properties {
1036 continue;
1037 }
1038 if let Ok(rel_defines) = decoder.decode_at(start, end) {
1039 if let Some(link) = collect_rel_defines_by_properties_link(&rel_defines) {
1040 rel_defines_by_properties.push(link);
1041 }
1042 }
1043 continue;
1044 } else if type_name.starts_with("IFCPROPERTY") {
1045 if !options.include_properties {
1046 continue;
1047 }
1048 if let Ok(property_entity) = decoder.decode_at(start, end) {
1049 if let Some((name, value)) = extract_property_name_and_value(&property_entity) {
1050 property_values_by_id.insert(id, (name, value));
1051 }
1052 }
1053 continue;
1054 } else if type_name == "IFCRELVOIDSELEMENT" {
1055 prepass_spans.void_rels.push((id, start, end));
1056 } else if type_name == "IFCRELFILLSELEMENT" {
1057 prepass_spans.fills_rels.push((id, start, end));
1058 } else if type_name == "IFCRELAGGREGATES" {
1059 prepass_spans.aggregate_rels.push((id, start, end));
1064 } else if type_name == "IFCPROJECT" && project_id.is_none() {
1065 project_id = Some(id);
1066 } else if type_name == "IFCSITE" && site_entity_pos.is_none() {
1067 site_entity_pos = Some((start, end));
1068 } else if type_name == "IFCBUILDING" && building_entity_pos.is_none() {
1069 building_entity_pos = Some((start, end));
1070 }
1071
1072 if ifc_lite_core::has_geometry_by_name(type_name) {
1073 let ifc_type = IfcType::from_str(type_name);
1074 if quick_metadata_enabled {
1075 quick_element_summaries.insert(
1076 id,
1077 QuickMetadataEntitySummary {
1078 express_id: id,
1079 type_name: type_name.to_string(),
1080 name: format!("{type_name} #{id}"),
1081 global_id: None,
1082 kind: "element".to_string(),
1083 has_children: false,
1084 element_count: None,
1085 elevation: None,
1086 },
1087 );
1088 }
1089 entity_jobs.push(EntityJob {
1090 id,
1091 ifc_type: ifc_type.clone(),
1092 start,
1093 end,
1094 product_definition_shape_id: None,
1095 element_color: crate::style::default_color_for_type(ifc_type).to_array(),
1096 global_id: None,
1097 name: None,
1098 presentation_layer: None,
1099 space_zone_properties: None,
1100 representation_map_id: None,
1101 });
1102 }
1103 else if type_name == "IFCMAPPEDITEM" {
1110 let args = parse_step_arguments(&content[start..end]);
1111 if let Some(source_id) = args.first().and_then(|token| parse_step_ref(token)) {
1112 referenced_representation_maps.insert(source_id);
1113 }
1114 } else if type_name == "IFCRELDEFINESBYTYPE" {
1115 let args = parse_step_arguments(&content[start..end]);
1118 if let Some(type_id) = args.get(5).and_then(|token| parse_step_ref(token)) {
1119 instantiated_type_ids.insert(type_id);
1120 }
1121 } else if (type_name.ends_with("TYPE") || type_name.ends_with("STYLE"))
1122 && IfcType::from_str(type_name).is_subtype_of(IfcType::IfcTypeProduct)
1123 {
1124 let args = parse_step_arguments(&content[start..end]);
1125 let rep_map_ids = args
1127 .get(6)
1128 .map(|token| parse_step_ref_list(token))
1129 .unwrap_or_default();
1130 if !rep_map_ids.is_empty() {
1131 type_product_geometry.push((
1132 id,
1133 start,
1134 end,
1135 IfcType::from_str(type_name),
1136 rep_map_ids,
1137 ));
1138 }
1139 }
1140 }
1141
1142 for (type_id, start, end, ifc_type, rep_map_ids) in &type_product_geometry {
1150 for (rep_map_id, _class) in crate::element::plan_type_geometry(
1155 rep_map_ids,
1156 &referenced_representation_maps,
1157 instantiated_type_ids.contains(type_id),
1158 crate::element::TypeGeometryMode::SuppressInstanced,
1159 ) {
1160 entity_jobs.push(EntityJob {
1161 id: *type_id,
1162 ifc_type: *ifc_type,
1163 start: *start,
1164 end: *end,
1165 product_definition_shape_id: None,
1166 element_color: crate::style::default_color_for_type(*ifc_type).to_array(),
1167 global_id: None,
1168 name: None,
1169 presentation_layer: None,
1170 space_zone_properties: None,
1171 representation_map_id: Some(rep_map_id),
1172 });
1173 }
1174 }
1175
1176 let resolved = crate::prepass::resolve_prepass(
1181 &prepass_spans,
1182 &mut decoder,
1183 crate::prepass::ResolveOptions {
1184 collect_indexed_colour_full: true,
1185 defer_attached_styles: defer_style_updates,
1186 },
1187 );
1188 let crate::prepass::ResolvedPrepass {
1189 mut geometry_style_index,
1190 indexed_colour_index,
1191 indexed_colour_full,
1192 element_material_colors,
1193 void_index,
1194 filling_by_opening,
1195 deferred_attached_styled_spans: deferred_styled_item_positions,
1196 ..
1197 } = resolved;
1198
1199 let entity_scan_time = entity_scan_start.elapsed();
1200
1201 let lookup_start = std::time::Instant::now();
1202 if options.include_properties {
1203 assign_space_zone_properties(
1204 &mut entity_jobs,
1205 &property_values_by_id,
1206 &property_sets_by_id,
1207 &rel_defines_by_properties,
1208 );
1209 }
1210 if options.fast_first_batch {
1211 entity_jobs.sort_by(|left, right| {
1212 geometry_priority_score(&right.ifc_type).cmp(&geometry_priority_score(&left.ifc_type))
1213 });
1214 }
1215 let lookup_time = lookup_start.elapsed();
1216
1217 let (skipped_entity_ids, filtered_void_index) = apply_opening_filter(
1218 &entity_jobs,
1219 &void_index,
1220 &filling_by_opening,
1221 &geometry_style_index,
1222 &mut decoder,
1223 opening_filter,
1224 );
1225
1226 if content
1228 .windows(b"IFC4X3".len())
1229 .any(|window| window == b"IFC4X3")
1230 {
1231 schema_version = "IFC4X3".into();
1232 } else if content
1233 .windows(b"IFC4".len())
1234 .any(|window| window == b"IFC4")
1235 {
1236 schema_version = "IFC4".into();
1237 }
1238
1239 let geometry_entity_count = entity_jobs.len();
1240 tracing::info!(
1241 total_entities = total_entities,
1242 geometry_entities = geometry_entity_count,
1243 voids = void_index.len(),
1244 schema_version = %schema_version,
1245 "Entity scanning complete"
1246 );
1247
1248 if let Some(mut spatial_nodes) = quick_spatial_nodes.take() {
1249 for (parent_id, child_ids) in quick_aggregate_links {
1250 if !spatial_nodes.contains_key(&parent_id) {
1251 continue;
1252 }
1253 for child_id in child_ids {
1254 if !spatial_nodes.contains_key(&child_id) {
1255 continue;
1256 }
1257 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1258 parent.children.push(child_id);
1259 }
1260 if let Some(child) = spatial_nodes.get_mut(&child_id) {
1261 child.parent = Some(parent_id);
1262 }
1263 }
1264 }
1265 for (parent_id, element_ids) in quick_containment_links {
1266 if !spatial_nodes.contains_key(&parent_id) {
1267 continue;
1268 }
1269 for child_id in element_ids {
1270 if spatial_nodes.contains_key(&child_id) {
1277 let already_placed = spatial_nodes
1280 .get(&child_id)
1281 .is_some_and(|child| child.parent.is_some());
1282 if !already_placed {
1283 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1284 parent.children.push(child_id);
1285 }
1286 if let Some(child) = spatial_nodes.get_mut(&child_id) {
1287 child.parent = Some(parent_id);
1288 }
1289 }
1290 } else if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1291 parent.elements.push(child_id);
1292 }
1293 }
1294 }
1295 for (parent_id, element_ids) in quick_referenced_links {
1299 if !spatial_nodes.contains_key(&parent_id) {
1300 continue;
1301 }
1302 for child_id in element_ids {
1303 if spatial_nodes.contains_key(&child_id) {
1306 continue;
1307 }
1308 if let Some(parent) = spatial_nodes.get_mut(&parent_id) {
1309 parent.elements.push(child_id);
1310 }
1311 }
1312 }
1313 let mut root_id = spatial_nodes
1314 .values()
1315 .find(|node| node.type_name == "IfcProject")
1316 .map(|node| node.express_id);
1317 if root_id.is_none() {
1318 root_id = spatial_nodes
1319 .values()
1320 .find(|node| node.parent.is_none())
1321 .map(|node| node.express_id);
1322 }
1323 let spatial_tree = root_id
1324 .map(|root| {
1325 build_quick_spatial_tree_node(root, &spatial_nodes, &quick_element_summaries)
1326 })
1327 .transpose()
1328 .unwrap_or(None);
1329 on_quick_metadata_bootstrap(&QuickMetadataBootstrap {
1330 schema_version: schema_version.clone(),
1331 entity_count: total_entities,
1332 spatial_tree,
1333 });
1334 }
1335
1336 let preprocess_start = std::time::Instant::now();
1338 let unit_scales = crate::prepass::resolve_unit_scales(content, project_id, &mut decoder);
1344 decoder.seed_unit_scales(
1345 unit_scales.length_unit_scale,
1346 unit_scales.plane_angle_to_radians,
1347 );
1348 let mut router = GeometryRouter::with_scale(unit_scales.length_unit_scale);
1349 router.set_tessellation_quality(options.tessellation_quality);
1350
1351 let site_transform: Option<Vec<f64>> = site_entity_pos.and_then(|(start, end)| {
1353 let entity = decoder.decode_at(start, end).ok()?;
1354 let matrix = router
1355 .resolve_scaled_placement(&entity, &mut decoder)
1356 .ok()?;
1357 Some(matrix.to_vec())
1358 });
1359 let building_transform: Option<Vec<f64>> = building_entity_pos.and_then(|(start, end)| {
1360 let entity = decoder.decode_at(start, end).ok()?;
1361 let matrix = router
1362 .resolve_scaled_placement(&entity, &mut decoder)
1363 .ok()?;
1364 Some(matrix.to_vec())
1365 });
1366
1367 let rtc_jobs: Vec<(u32, usize, usize, IfcType)> = entity_jobs
1368 .iter()
1369 .map(|job| (job.id, job.start, job.end, job.ifc_type))
1370 .collect();
1371 let detected_rtc_offset =
1372 router.detect_rtc_offset_with_fallback(&rtc_jobs, &mut decoder, content);
1373
1374 let site_rtc = site_transform
1383 .as_ref()
1384 .map(|st| (st[12], st[13], st[14])) .filter(|t| translation_is_nonidentity(*t));
1386 let detected_has_offset = translation_is_nonidentity(detected_rtc_offset);
1387 let (rtc_offset, coord_space) = if let Some(site) = site_rtc {
1388 (site, SITE_LOCAL_MESH_COORDINATE_SPACE)
1389 } else if detected_has_offset {
1390 (detected_rtc_offset, MODEL_RTC_MESH_COORDINATE_SPACE)
1391 } else {
1392 ((0.0, 0.0, 0.0), RAW_IFC_MESH_COORDINATE_SPACE)
1393 };
1394 let has_rtc_offset = coord_space != RAW_IFC_MESH_COORDINATE_SPACE;
1395 router.set_rtc_offset(rtc_offset);
1396 let preprocess_time = preprocess_start.elapsed();
1397
1398 let parse_time = parse_start.elapsed();
1399 tracing::info!(
1400 entity_scan_time_ms = entity_scan_time.as_millis(),
1401 lookup_time_ms = lookup_time.as_millis(),
1402 preprocess_time_ms = preprocess_time.as_millis(),
1403 parse_time_ms = parse_time.as_millis(),
1404 "Parse phase complete, starting geometry extraction"
1405 );
1406
1407 let geometry_start = std::time::Instant::now();
1409 let entity_index_arc = entity_index; let unit_scale = router.unit_scale();
1411 let rtc_offset = router.rtc_offset();
1412 let seed_plane_angle_to_radians = unit_scales.plane_angle_to_radians;
1420 let void_index_arc = Arc::new(filtered_void_index);
1421 let skipped_entity_ids = Arc::new(skipped_entity_ids);
1422 crate::prepass::merge_indexed_colours(&mut geometry_style_index, &indexed_colour_index);
1425 let mut geometry_style_index = Arc::new(geometry_style_index);
1426 let indexed_colour_full = Arc::new(indexed_colour_full);
1427 let texture_index = Arc::new(ifc_lite_geometry::build_texture_index(
1432 content,
1433 &mut decoder,
1434 ));
1435 let element_material_color: FxHashMap<u32, [f32; 4]> = element_material_colors
1439 .iter()
1440 .filter_map(|(&id, colors)| crate::style::pick_opaque_first(colors).map(|c| (id, c)))
1441 .collect();
1442 let element_material_colors = Arc::new(element_material_colors);
1443
1444 let total_jobs = entity_jobs.len();
1445 let initial_chunk_size = options.initial_batch_size.max(1);
1446 let throughput_chunk_size = options.throughput_batch_size.max(initial_chunk_size);
1447 let mut color_cache_by_product_definition_shape: FxHashMap<u32, Option<[f32; 4]>> =
1448 FxHashMap::default();
1449 let mut layer_cache_by_product_definition_shape: FxHashMap<u32, Option<String>> =
1450 FxHashMap::default();
1451 let mut layer_cache_by_representation: FxHashMap<u32, Option<String>> = FxHashMap::default();
1452 let mut meshes: Vec<MeshData> = Vec::new();
1453 let mut processed_jobs = 0usize;
1454 let mut total_meshes = 0usize;
1455 let mut total_vertices = 0usize;
1456 let mut total_triangles = 0usize;
1457 let mut chunk_start = 0usize;
1458 let mut current_chunk_size = initial_chunk_size;
1459
1460 let mut deferred_styles_applied = !defer_style_updates;
1461
1462 let csg_failure_collector: std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>> =
1465 std::sync::Mutex::new(FxHashMap::default());
1466
1467 while chunk_start < total_jobs {
1468 let chunk_end = (chunk_start + current_chunk_size).min(total_jobs);
1469 let jobs_chunk = &mut entity_jobs[chunk_start..chunk_end];
1470
1471 #[cfg(not(target_arch = "wasm32"))]
1475 {
1476 let entity_index_for_meta = entity_index_arc.clone();
1478 jobs_chunk.par_iter_mut().for_each(|job| {
1479 if job.global_id.is_some()
1480 || job.name.is_some()
1481 || job.product_definition_shape_id.is_some()
1482 {
1483 return;
1484 }
1485 let mut local_decoder =
1486 EntityDecoder::with_arc_index(content, entity_index_for_meta.clone());
1487 let Ok(entity) = local_decoder.decode_at(job.start, job.end) else {
1488 return;
1489 };
1490 job.global_id = normalize_optional_string(entity.get_string(0));
1491 job.name = normalize_optional_string(entity.get_string(2));
1492 job.product_definition_shape_id = entity.get_ref(6);
1493 });
1494
1495 for job in jobs_chunk.iter_mut() {
1497 let Some(pds_id) = job.product_definition_shape_id else {
1498 continue;
1499 };
1500 let resolved_color = color_cache_by_product_definition_shape
1501 .entry(pds_id)
1502 .or_insert_with(|| {
1503 resolve_element_color_for_product_definition_shape(
1504 pds_id,
1505 &geometry_style_index,
1506 &mut decoder,
1507 )
1508 });
1509 if let Some(color) = resolved_color {
1510 job.element_color = *color;
1511 } else if let Some(color) = element_material_color.get(&job.id) {
1512 job.element_color = *color;
1515 }
1516 if options.include_presentation_layers {
1517 let resolved_layer = layer_cache_by_product_definition_shape
1518 .entry(pds_id)
1519 .or_insert_with(|| {
1520 resolve_presentation_layer_for_product_definition_shape(
1521 pds_id,
1522 &presentation_layer_by_assigned_id,
1523 &mut layer_cache_by_representation,
1524 &mut decoder,
1525 )
1526 });
1527 job.presentation_layer = resolved_layer.clone();
1528 }
1529 }
1530 }
1531
1532 #[cfg(target_arch = "wasm32")]
1534 for job in jobs_chunk.iter_mut() {
1535 populate_entity_job_metadata(
1536 job,
1537 &geometry_style_index,
1538 &element_material_color,
1539 &presentation_layer_by_assigned_id,
1540 &mut color_cache_by_product_definition_shape,
1541 &mut layer_cache_by_product_definition_shape,
1542 &mut layer_cache_by_representation,
1543 &mut decoder,
1544 options.include_presentation_layers,
1545 );
1546 }
1547 let site_local_rotation: Option<&Vec<f64>> =
1548 if coord_space == SITE_LOCAL_MESH_COORDINATE_SPACE {
1549 site_transform.as_ref()
1550 } else {
1551 None
1552 };
1553 let chunk_meshes: Vec<MeshData> = jobs_chunk
1554 .par_iter()
1555 .flat_map_iter(|job| {
1556 process_entity_job(
1557 job,
1558 content,
1559 &entity_index_arc,
1560 unit_scale,
1561 rtc_offset,
1562 seed_plane_angle_to_radians,
1563 options.tessellation_quality,
1564 void_index_arc.as_ref(),
1565 skipped_entity_ids.as_ref(),
1566 geometry_style_index.as_ref(),
1567 indexed_colour_full.as_ref(),
1568 element_material_colors.as_ref(),
1569 texture_index.as_ref(),
1570 site_local_rotation,
1571 &csg_failure_collector,
1572 )
1573 })
1574 .collect();
1575
1576 processed_jobs += jobs_chunk.len();
1577 total_vertices += chunk_meshes.iter().map(|m| m.vertex_count()).sum::<usize>();
1578 total_triangles += chunk_meshes
1579 .iter()
1580 .map(|m| m.triangle_count())
1581 .sum::<usize>();
1582
1583 if !chunk_meshes.is_empty() {
1584 total_meshes += chunk_meshes.len();
1585 let emit_mesh_chunk_size = current_chunk_size.max(1);
1586 for emitted_meshes in chunk_meshes.chunks(emit_mesh_chunk_size) {
1587 on_batch(emitted_meshes, processed_jobs, total_jobs);
1588 }
1589 if options.retain_emitted_meshes {
1590 meshes.extend(chunk_meshes);
1591 }
1592
1593 if !deferred_styles_applied {
1594 let mut rebuilt_styles = {
1599 let mut style_decoder =
1600 EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1601 crate::prepass::resolve_styled_item_spans(
1602 &deferred_styled_item_positions,
1603 &mut style_decoder,
1604 )
1605 };
1606 crate::prepass::merge_indexed_colours(&mut rebuilt_styles, &indexed_colour_index);
1607 geometry_style_index = Arc::new(rebuilt_styles);
1608 let deferred_color_updates = build_color_updates_for_jobs(
1609 &entity_jobs[..processed_jobs],
1610 geometry_style_index.as_ref(),
1611 content,
1612 &entity_index_arc,
1613 );
1614 if !deferred_color_updates.is_empty() {
1615 on_color_update(&deferred_color_updates);
1616 }
1617 deferred_styles_applied = true;
1618 }
1619 }
1620 chunk_start = chunk_end;
1621 current_chunk_size = throughput_chunk_size;
1622 }
1623
1624 let geometry_time = geometry_start.elapsed();
1625 let csg_failures = csg_failure_collector
1628 .into_inner()
1629 .unwrap_or_else(|poisoned| poisoned.into_inner());
1630 let total_csg_failures: usize = csg_failures.values().map(Vec::len).sum();
1631 let products_with_failures = csg_failures.len();
1632 if total_csg_failures > 0 {
1633 let mut by_reason: HashMap<&'static str, usize> = HashMap::new();
1634 for fails in csg_failures.values() {
1635 for f in fails {
1636 *by_reason.entry(f.reason.label()).or_insert(0) += 1;
1637 }
1638 }
1639 let mut breakdown: Vec<(&'static str, usize)> = by_reason.into_iter().collect();
1640 breakdown.sort_by(|a, b| b.1.cmp(&a.1));
1641 let breakdown = breakdown
1642 .iter()
1643 .map(|(reason, count)| format!("{reason}={count}"))
1644 .collect::<Vec<_>>()
1645 .join(" ");
1646 tracing::warn!(
1647 total_csg_failures,
1648 products_with_failures,
1649 %breakdown,
1650 "CSG failures during geometry extraction (cut dropped, host kept uncut)"
1651 );
1652 }
1653
1654 let total_time = total_start.elapsed();
1655
1656 tracing::info!(
1657 meshes = meshes.len(),
1658 vertices = total_vertices,
1659 triangles = total_triangles,
1660 geometry_time_ms = geometry_time.as_millis(),
1661 total_time_ms = total_time.as_millis(),
1662 "Geometry processing complete"
1663 );
1664
1665 ProcessingResult {
1666 meshes,
1667 mesh_coordinate_space: Some(coord_space.to_string()),
1668 site_transform,
1669 building_transform,
1670 metadata: ModelMetadata {
1671 schema_version,
1672 entity_count: total_entities,
1673 geometry_entity_count,
1674 coordinate_info: CoordinateInfo {
1675 origin_shift: [rtc_offset.0, rtc_offset.1, rtc_offset.2],
1676 is_geo_referenced: has_rtc_offset,
1677 },
1678 length_unit_scale: Some(unit_scale),
1679 georeferencing: crate::extract_georeferencing(content),
1680 },
1681 stats: ProcessingStats {
1682 total_meshes,
1683 total_vertices,
1684 total_triangles,
1685 parse_time_ms: parse_time.as_millis() as u64,
1686 entity_scan_time_ms: entity_scan_time.as_millis() as u64,
1687 lookup_time_ms: lookup_time.as_millis() as u64,
1688 preprocess_time_ms: preprocess_time.as_millis() as u64,
1689 geometry_time_ms: geometry_time.as_millis() as u64,
1690 total_time_ms: total_time.as_millis() as u64,
1691 from_cache: false,
1692 total_csg_failures: total_csg_failures as u64,
1693 products_with_failures: products_with_failures as u64,
1694 },
1695 }
1696}
1697
1698fn process_entity_job(
1699 job: &EntityJob,
1700 content: &[u8],
1701 entity_index_arc: &Arc<EntityIndex>,
1702 unit_scale: f64,
1703 rtc_offset: (f64, f64, f64),
1704 seed_plane_angle_to_radians: f64,
1707 tessellation_quality: TessellationQuality,
1708 void_index: &FxHashMap<u32, Vec<u32>>,
1709 skipped_entity_ids: &HashSet<u32>,
1710 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
1711 indexed_colour_full: &FxHashMap<u32, crate::style::FullIndexedColourMap>,
1712 element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
1713 texture_index: &FxHashMap<u32, ifc_lite_geometry::ResolvedTextureMap>,
1716 site_local_rotation: Option<&Vec<f64>>,
1719 csg_failure_collector: &std::sync::Mutex<FxHashMap<u32, Vec<ifc_lite_geometry::BoolFailure>>>,
1722) -> Vec<MeshData> {
1723 if skipped_entity_ids.contains(&job.id) {
1724 return Vec::new();
1725 }
1726
1727 let mut local_decoder = EntityDecoder::with_arc_index(content, entity_index_arc.clone());
1728 local_decoder.seed_unit_scales(unit_scale, seed_plane_angle_to_radians);
1731
1732 let entity = match local_decoder.decode_at(job.start, job.end) {
1733 Ok(entity) => entity,
1734 Err(_) => return Vec::new(),
1735 };
1736
1737 let mut local_router = GeometryRouter::with_scale_and_quality(unit_scale, tessellation_quality);
1738 local_router.set_rtc_offset(rtc_offset);
1739 let local_router = local_router;
1740
1741 let metadata = crate::element::ElementMeshMetadata {
1742 global_id: job.global_id.clone(),
1743 name: job.name.clone(),
1744 presentation_layer: job.presentation_layer.clone(),
1745 space_zone_properties: job.space_zone_properties.clone(),
1746 };
1747 let kind = match job.representation_map_id {
1751 Some(rep_map_id) => crate::element::ElementJobKind::TypeProduct {
1752 rep_maps: vec![(rep_map_id, 1)],
1753 },
1754 None => crate::element::ElementJobKind::Product,
1755 };
1756 let ctx = crate::element::MeshProductionContext {
1757 void_index,
1758 geometry_style_index,
1759 indexed_colour_full,
1760 element_material_colors,
1761 texture_index,
1762 site_local_rotation,
1763 };
1764
1765 let produced = crate::element::produce_element_meshes(
1766 &crate::element::ElementMeshJob {
1767 id: job.id,
1768 ifc_type: job.ifc_type,
1769 entity: &entity,
1770 kind,
1771 element_color: Some(job.element_color),
1772 metadata: Some(&metadata),
1773 },
1774 &ctx,
1775 &crate::element::MeshProductionOptions::default(),
1777 &mut local_decoder,
1778 &local_router,
1779 );
1780
1781 if !produced.csg_failures.is_empty() {
1785 if let Ok(mut collector) = csg_failure_collector.lock() {
1786 for (product_id, fails) in produced.csg_failures {
1787 collector.entry(product_id).or_default().extend(fails);
1788 }
1789 }
1790 }
1791
1792 produced.meshes
1793}
1794
1795
1796
1797fn build_color_updates_for_jobs(
1798 jobs: &[EntityJob],
1799 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
1800 content: &[u8],
1801 entity_index: &Arc<EntityIndex>,
1802) -> Vec<(u32, [f32; 4])> {
1803 let mut decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
1804 let mut updates: Vec<(u32, [f32; 4])> = Vec::new();
1805
1806 for job in jobs {
1807 if let Some(rep_map_id) = job.representation_map_id {
1813 if let Some(color) = crate::element::resolve_color_for_representation_map(
1814 rep_map_id,
1815 geometry_styles,
1816 &mut decoder,
1817 ) {
1818 if color != job.element_color {
1819 updates.push((job.id, color));
1820 }
1821 }
1822 continue;
1823 }
1824 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
1825 continue;
1826 };
1827 let Some(product_definition_shape_id) = entity.get_ref(6) else {
1828 continue;
1829 };
1830 let Some(color) = resolve_element_color_for_product_definition_shape(
1831 product_definition_shape_id,
1832 geometry_styles,
1833 &mut decoder,
1834 ) else {
1835 continue;
1836 };
1837 if color != job.element_color {
1838 updates.push((job.id, color));
1839 }
1840 }
1841
1842 updates
1843}
1844
1845fn collect_presentation_layer_assignments(
1846 layer_by_assigned_representation: &mut FxHashMap<u32, String>,
1847 layer_assignment: &DecodedEntity,
1848) {
1849 let Some(layer_name) = normalize_optional_string(layer_assignment.get_string(0)) else {
1850 return;
1851 };
1852
1853 let Some(assigned_items) = get_refs_from_list(layer_assignment, 2) else {
1854 return;
1855 };
1856
1857 for assigned in assigned_items {
1858 layer_by_assigned_representation
1859 .entry(assigned)
1860 .or_insert_with(|| layer_name.clone());
1861 }
1862}
1863
1864fn resolve_element_color_for_product_definition_shape(
1865 product_definition_shape_id: u32,
1866 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
1867 decoder: &mut EntityDecoder,
1868) -> Option<[f32; 4]> {
1869 find_color_in_representation(product_definition_shape_id, geometry_styles, decoder)
1870}
1871
1872fn resolve_presentation_layer_for_product_definition_shape(
1873 product_definition_shape_id: u32,
1874 layer_by_assigned_representation: &FxHashMap<u32, String>,
1875 cache_by_representation: &mut FxHashMap<u32, Option<String>>,
1876 decoder: &mut EntityDecoder,
1877) -> Option<String> {
1878 if let Some(layer_name) = layer_by_assigned_representation.get(&product_definition_shape_id) {
1879 return Some(layer_name.clone());
1880 }
1881
1882 let product_definition_shape = decoder.decode_by_id(product_definition_shape_id).ok()?;
1883 let representation_ids = get_refs_from_list(&product_definition_shape, 2)?;
1884
1885 for representation_id in representation_ids {
1886 if let Some(layer_name) = resolve_presentation_layer_name(
1887 representation_id,
1888 layer_by_assigned_representation,
1889 cache_by_representation,
1890 decoder,
1891 &mut Vec::new(),
1892 ) {
1893 return Some(layer_name);
1894 }
1895 }
1896
1897 None
1898}
1899
1900fn resolve_presentation_layer_name(
1901 representation_id: u32,
1902 layer_by_assigned_representation: &FxHashMap<u32, String>,
1903 cache_by_representation: &mut FxHashMap<u32, Option<String>>,
1904 decoder: &mut EntityDecoder,
1905 traversal_stack: &mut Vec<u32>,
1906) -> Option<String> {
1907 if let Some(cached) = cache_by_representation.get(&representation_id) {
1908 return cached.clone();
1909 }
1910
1911 if traversal_stack.contains(&representation_id) {
1912 return None;
1913 }
1914 traversal_stack.push(representation_id);
1915
1916 if let Some(layer_name) = layer_by_assigned_representation.get(&representation_id) {
1917 let result = Some(layer_name.clone());
1918 cache_by_representation.insert(representation_id, result.clone());
1919 traversal_stack.pop();
1920 return result;
1921 }
1922
1923 let mut resolved: Option<String> = None;
1924
1925 if let Ok(representation) = decoder.decode_by_id(representation_id) {
1926 if let Some(items) = get_refs_from_list(&representation, 3) {
1927 for item_id in items {
1928 if let Some(layer_name) = layer_by_assigned_representation.get(&item_id) {
1929 resolved = Some(layer_name.clone());
1930 break;
1931 }
1932
1933 if let Ok(item) = decoder.decode_by_id(item_id) {
1934 if item.ifc_type == IfcType::IfcMappedItem {
1935 if let Some(mapping_source_id) = item.get_ref(0) {
1936 if let Ok(mapping_source) = decoder.decode_by_id(mapping_source_id) {
1937 if let Some(mapped_representation_id) = mapping_source.get_ref(1) {
1938 if let Some(layer_name) = resolve_presentation_layer_name(
1939 mapped_representation_id,
1940 layer_by_assigned_representation,
1941 cache_by_representation,
1942 decoder,
1943 traversal_stack,
1944 ) {
1945 resolved = Some(layer_name);
1946 break;
1947 }
1948 }
1949 }
1950 }
1951 }
1952 }
1953 }
1954 }
1955 }
1956
1957 traversal_stack.pop();
1958 cache_by_representation.insert(representation_id, resolved.clone());
1959 resolved
1960}
1961
1962fn find_color_in_representation(
1964 repr_id: u32,
1965 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
1966 decoder: &mut EntityDecoder,
1967) -> Option<[f32; 4]> {
1968 let repr = decoder.decode_by_id(repr_id).ok()?;
1970
1971 let repr_list = get_refs_from_list(&repr, 2)?;
1973
1974 for shape_repr_id in repr_list {
1975 if let Ok(shape_repr) = decoder.decode_by_id(shape_repr_id) {
1976 if let Some(items) = get_refs_from_list(&shape_repr, 3) {
1978 for item_id in items {
1979 if let Some(style) = geometry_styles.get(&item_id) {
1981 return Some(style.color);
1982 }
1983
1984 if let Ok(item) = decoder.decode_by_id(item_id) {
1986 if item.ifc_type == IfcType::IfcMappedItem {
1987 if let Some(source_id) = item.get_ref(0) {
1988 if let Ok(source) = decoder.decode_by_id(source_id) {
1989 if let Some(mapped_repr_id) = source.get_ref(1) {
1990 if let Some(color) = find_color_in_shape_representation(
1991 mapped_repr_id,
1992 geometry_styles,
1993 decoder,
1994 ) {
1995 return Some(color);
1996 }
1997 }
1998 }
1999 }
2000 }
2001 }
2002 }
2003 }
2004 }
2005 }
2006
2007 None
2008}
2009
2010fn find_color_in_shape_representation(
2012 repr_id: u32,
2013 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
2014 decoder: &mut EntityDecoder,
2015) -> Option<[f32; 4]> {
2016 let repr = decoder.decode_by_id(repr_id).ok()?;
2017 let items = get_refs_from_list(&repr, 3)?;
2018
2019 for item_id in items {
2020 if let Some(style) = geometry_styles.get(&item_id) {
2021 return Some(style.color);
2022 }
2023 }
2024
2025 None
2026}
2027
2028fn apply_opening_filter(
2034 entity_jobs: &[EntityJob],
2035 void_index: &FxHashMap<u32, Vec<u32>>,
2036 filling_by_opening: &FxHashMap<u32, u32>,
2037 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
2038 decoder: &mut EntityDecoder,
2039 mode: OpeningFilterMode,
2040) -> (HashSet<u32>, FxHashMap<u32, Vec<u32>>) {
2041 if mode == OpeningFilterMode::Default {
2042 return (HashSet::default(), void_index.clone());
2043 }
2044
2045 let filling_jobs: FxHashMap<u32, &EntityJob> = entity_jobs
2047 .iter()
2048 .filter(|job| matches!(job.ifc_type, IfcType::IfcWindow | IfcType::IfcDoor))
2049 .map(|job| (job.id, job))
2050 .collect();
2051
2052 if filling_jobs.is_empty() {
2053 return (HashSet::default(), void_index.clone());
2054 }
2055
2056 let mut skipped_entity_ids: HashSet<u32> = HashSet::default();
2057
2058 if mode == OpeningFilterMode::IgnoreAll {
2063 for (&id, _) in &filling_jobs {
2064 skipped_entity_ids.insert(id);
2065 }
2066 return (skipped_entity_ids, FxHashMap::default());
2067 }
2068
2069 for (&id, job) in &filling_jobs {
2073 if is_opaque_opening(job, geometry_style_index, decoder) {
2074 skipped_entity_ids.insert(id);
2075 }
2076 }
2077
2078 if filling_by_opening.is_empty() {
2079 return (skipped_entity_ids, void_index.clone());
2081 }
2082
2083 let mut openings_to_suppress: HashSet<u32> = HashSet::default();
2085 for (&opening_id, &filling_id) in filling_by_opening {
2086 if skipped_entity_ids.contains(&filling_id) {
2087 openings_to_suppress.insert(opening_id);
2088 }
2089 }
2090
2091 if openings_to_suppress.is_empty() {
2092 return (skipped_entity_ids, void_index.clone());
2093 }
2094
2095 let mut filtered: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
2096 for (&host_id, openings) in void_index {
2097 let remaining: Vec<u32> = openings
2098 .iter()
2099 .copied()
2100 .filter(|oid| !openings_to_suppress.contains(oid))
2101 .collect();
2102 if !remaining.is_empty() {
2103 filtered.insert(host_id, remaining);
2104 }
2105 }
2106
2107 (skipped_entity_ids, filtered)
2108}
2109
2110fn is_opaque_opening(
2118 job: &EntityJob,
2119 styles: &FxHashMap<u32, GeometryStyleInfo>,
2120 decoder: &mut EntityDecoder,
2121) -> bool {
2122 let Ok(entity) = decoder.decode_at(job.start, job.end) else {
2123 return true;
2124 };
2125
2126 if normalize_optional_string(entity.get_string(2))
2128 .as_deref()
2129 .map(|n| n.to_lowercase().contains("glas"))
2130 .unwrap_or(false)
2131 {
2132 return false;
2133 }
2134
2135 if job.element_color[3] < 1.0 {
2139 return false;
2140 }
2141
2142 let Some(product_shape_id) = entity.get_ref(6) else {
2143 return true; };
2145
2146 let Ok(product_shape) = decoder.decode_by_id(product_shape_id) else {
2147 return true;
2148 };
2149
2150 let Some(repr_ids) = get_refs_from_list(&product_shape, 2) else {
2151 return true;
2152 };
2153
2154 for repr_id in repr_ids {
2155 let Ok(repr) = decoder.decode_by_id(repr_id) else {
2156 continue;
2157 };
2158 let Some(item_ids) = get_refs_from_list(&repr, 3) else {
2159 continue;
2160 };
2161 for item_id in item_ids {
2162 if let Some(style) = styles.get(&item_id) {
2164 if has_glass_style(style) {
2165 return false;
2166 }
2167 }
2168
2169 if let Ok(item) = decoder.decode_by_id(item_id) {
2171 if item.ifc_type == IfcType::IfcMappedItem {
2172 if let Some(source_id) = item.get_ref(0) {
2173 if let Ok(source) = decoder.decode_by_id(source_id) {
2174 if let Some(mapped_repr_id) = source.get_ref(1) {
2175 if let Ok(mapped_repr) = decoder.decode_by_id(mapped_repr_id) {
2176 if let Some(mapped_items) = get_refs_from_list(&mapped_repr, 3)
2177 {
2178 for mapped_item_id in mapped_items {
2179 if let Some(style) = styles.get(&mapped_item_id) {
2180 if has_glass_style(style) {
2181 return false;
2182 }
2183 }
2184 }
2185 }
2186 }
2187 }
2188 }
2189 }
2190 }
2191 }
2192 }
2193 }
2194
2195 true }
2197
2198fn has_glass_style(style: &GeometryStyleInfo) -> bool {
2204 if style.color[3] < 1.0 {
2205 return true;
2206 }
2207 if style
2208 .material_name
2209 .as_deref()
2210 .map(|n| n.to_lowercase().contains("glas"))
2211 .unwrap_or(false)
2212 {
2213 return true;
2214 }
2215 false
2216}
2217
2218
2219#[cfg(test)]
2224mod tests {
2225 use super::*;
2226
2227 fn map(pairs: &[(u32, &[u32])]) -> FxHashMap<u32, Vec<u32>> {
2228 pairs.iter().map(|(k, v)| (*k, v.to_vec())).collect()
2229 }
2230
2231 }