ifc_lite_server/services/data_model/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Data model extraction service - extracts properties, relationships, and spatial hierarchy.
6
7mod classifications;
8mod generated;
9mod documents;
10mod materials;
11mod material_units;
12mod metadata;
13mod properties;
14mod property_value;
15mod quantities;
16mod relationships;
17mod rooted;
18mod spatial;
19mod types;
20
21pub use rooted::DataModelEntities;
22pub use types::*;
23
24use classifications::extract_classifications;
25use documents::extract_documents;
26use ifc_lite_core::{
27    build_entity_index, extract_length_unit_scale, DecodedEntity, EntityDecoder, EntityScanner,
28};
29use materials::extract_materials;
30use material_units::MaterialUnitContext;
31use metadata::extract_entity_metadata;
32use properties::extract_properties;
33use quantities::extract_quantities;
34use relationships::extract_relationships;
35use spatial::build_spatial_hierarchy;
36use std::sync::Arc;
37
38/// Extract complete data model from IFC content.
39pub fn extract_data_model<T>(content: &T) -> DataModel
40where
41    T: AsRef<[u8]> + ?Sized,
42{
43    let content = content.as_ref();
44    let extract_start = std::time::Instant::now();
45    tracing::info!(
46        content_size = content.len(),
47        "Starting data model extraction"
48    );
49
50    // Build entity index (shared across all extractors)
51    let entity_index = Arc::new(build_entity_index(content));
52
53    // Scan all entities once
54    let mut scanner = EntityScanner::new(content);
55    let mut all_entities: Vec<EntityJob> = Vec::new();
56    let mut total_entities = 0usize;
57
58    let mut last_id = 0u32;
59    let mut last_type = String::new();
60    let mut max_id = 0u32;
61    let mut last_end = 0usize;
62    let content_len = content.len();
63
64    while let Some((id, type_name, start, end)) = scanner.next_entity() {
65        total_entities += 1;
66        last_id = id;
67        last_type = type_name.to_string();
68        last_end = end;
69        if id > max_id {
70            max_id = id;
71        }
72        all_entities.push(EntityJob {
73            id,
74            type_name: type_name.to_string(),
75            start,
76            end,
77        });
78    }
79
80    let remaining_bytes = content_len.saturating_sub(last_end);
81    tracing::debug!(
82        total_entities = total_entities,
83        last_id = last_id,
84        max_id = max_id,
85        last_type = %last_type,
86        last_end = last_end,
87        content_len = content_len,
88        remaining_bytes = remaining_bytes,
89        "Scanned all entities"
90    );
91
92    // Debug: log sample entity types to diagnose issues
93    if tracing::enabled!(tracing::Level::DEBUG) {
94        let sample_types: Vec<&str> = all_entities
95            .iter()
96            .take(20)
97            .map(|j| j.type_name.as_str())
98            .collect();
99        tracing::debug!(?sample_types, "Sample entity types from scan");
100
101        // Check if any type contains "PROPERTY" or "REL" (case-insensitive)
102        let has_property_like = all_entities
103            .iter()
104            .any(|j| j.type_name.to_uppercase().contains("PROPERTY"));
105        let has_rel_like = all_entities
106            .iter()
107            .any(|j| j.type_name.to_uppercase().starts_with("IFCREL"));
108        tracing::debug!(
109            has_property_like = has_property_like,
110            has_rel_like = has_rel_like,
111            "Entity type pattern check"
112        );
113
114        // Debug: count property sets and relationships in scanned entities
115        let pset_count = all_entities
116            .iter()
117            .filter(|j| j.type_name.to_uppercase() == "IFCPROPERTYSET")
118            .count();
119        let rel_count = all_entities
120            .iter()
121            .filter(|j| {
122                let t = j.type_name.to_uppercase();
123                t == "IFCRELDEFINESBYPROPERTIES"
124                    || t == "IFCRELAGGREGATES"
125                    || t == "IFCRELCONTAINEDINSPATIALSTRUCTURE"
126            })
127            .count();
128        tracing::debug!(
129            pset_count = pset_count,
130            rel_count = rel_count,
131            "Entity type counts before extraction"
132        );
133    }
134
135    // Parallel extraction using rayon::join
136    let content_arc = Arc::new(content.to_vec());
137    let (entities, ((property_sets, quantity_sets), relationships)) = rayon::join(
138        || extract_entity_metadata(&all_entities, &content_arc, &entity_index),
139        || {
140            rayon::join(
141                || {
142                    rayon::join(
143                        || extract_properties(&all_entities, &content_arc, &entity_index),
144                        || extract_quantities(&all_entities, &content_arc, &entity_index),
145                    )
146                },
147                || extract_relationships(&all_entities, &content_arc, &entity_index),
148            )
149        },
150    );
151
152    // Extract length unit scale (e.g., 0.001 for millimeters)
153    let mut unit_decoder = EntityDecoder::with_arc_index(content, entity_index.clone());
154    let project_id_for_units = entities
155        .iter()
156        .find(|e| e.type_name.to_uppercase() == "IFCPROJECT")
157        .map(|e| e.entity_id)
158        .unwrap_or(0);
159    let length_unit_scale = if project_id_for_units > 0 {
160        extract_length_unit_scale(&mut unit_decoder, project_id_for_units).unwrap_or(1.0)
161    } else {
162        1.0
163    };
164    tracing::debug!(
165        length_unit_scale = length_unit_scale,
166        "Extracted length unit scale"
167    );
168    let material_units = MaterialUnitContext::new(
169        &entities, &relationships, &mut unit_decoder, length_unit_scale,
170    );
171
172    // Extract classifications, materials, and documents in parallel. These
173    // follow the same `IfcRelAssociates*` pattern as the relationship pass but
174    // resolve the referenced object (classification reference, material layer
175    // set, document) into a flat, element-keyed shape (issue #900 parity).
176    // Materials need the length-unit scale to report layer thickness in metres.
177    let ((classifications, materials), documents) = rayon::join(
178        || {
179            rayon::join(
180                || extract_classifications(&all_entities, &content_arc, &entity_index),
181                || {
182                    extract_materials(
183                        &all_entities,
184                        &content_arc,
185                        &entity_index,
186                        &material_units,
187                    )
188                },
189            )
190        },
191        || extract_documents(&all_entities, &content_arc, &entity_index),
192    );
193
194    // Build spatial hierarchy (depends on relationships and entities)
195    let spatial_hierarchy = build_spatial_hierarchy(
196        &relationships,
197        &entities,
198        content,
199        &entity_index,
200        length_unit_scale,
201    );
202
203    let extract_time = extract_start.elapsed();
204    tracing::info!(
205        entities = entities.len(),
206        property_sets = property_sets.len(),
207        quantity_sets = quantity_sets.len(),
208        relationships = relationships.len(),
209        classifications = classifications.len(),
210        materials = materials.len(),
211        documents = documents.len(),
212        spatial_nodes = spatial_hierarchy.nodes.len(),
213        extract_time_ms = extract_time.as_millis(),
214        "Data model extraction complete"
215    );
216
217    DataModel {
218        entities,
219        property_sets,
220        quantity_sets,
221        relationships,
222        classifications,
223        materials,
224        documents,
225        spatial_hierarchy,
226    }
227}
228
229/// Read an `IfcLogical` / `IfcBoolean` attribute as a tri-state `Option<bool>`
230/// (`.U.` / absent → `None`).
231pub(super) fn read_logical(entity: &DecodedEntity, index: usize) -> Option<bool> {
232    let token = entity.get(index)?.as_enum()?;
233    match token {
234        "T" | "TRUE" | "true" => Some(true),
235        "F" | "FALSE" | "false" => Some(false),
236        _ => None,
237    }
238}
239
240/// Collect the `RelatedObjects` (attribute 4) entity ids of an `IfcRelAssociates*`.
241pub(super) fn related_object_ids(rel: &DecodedEntity) -> Vec<u32> {
242    rel.get_list(4)
243        .map(|list| list.iter().filter_map(|v| v.as_entity_ref()).collect())
244        .unwrap_or_default()
245}
246
247
248#[cfg(test)]
249#[path = "tests.rs"]
250mod tests;