Skip to main content

bimifc_parser/ifcx/
geometry.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//! IFC5 (IFCX) geometry extraction
6//!
7//! Extracts pre-tessellated USD mesh geometry from IFCX nodes.
8//! Unlike IFC4 which requires complex geometry processing,
9//! IFC5 provides ready-to-use triangulated meshes.
10
11use super::model::IfcxModel;
12use super::types::{attr, Transform4x4, UsdMesh};
13use bimifc_model::{
14    get_default_color, EntityGeometry, EntityId, GeometrySource, IfcModel, MeshData,
15};
16use rustc_hash::FxHashMap;
17use std::sync::Arc;
18
19/// Geometry source for IFCX models
20pub struct IfcxGeometry {
21    /// Reference to the model
22    model: Arc<IfcxModel>,
23    /// Cached geometry by entity ID (for future use)
24    #[allow(dead_code)]
25    cache: FxHashMap<EntityId, Option<EntityGeometry>>,
26    /// Entity IDs with geometry
27    entities_with_geom: Vec<EntityId>,
28}
29
30impl IfcxGeometry {
31    /// Create geometry source from IFCX model
32    pub fn new(model: Arc<IfcxModel>) -> Self {
33        // Find all entities with geometry
34        let mut entities_with_geom = Vec::new();
35
36        for id in model.resolver().all_ids() {
37            if let Some(node) = model.node(id) {
38                if node.attributes.contains_key(attr::MESH) {
39                    entities_with_geom.push(id);
40                }
41            }
42        }
43
44        Self {
45            model,
46            cache: FxHashMap::default(),
47            entities_with_geom,
48        }
49    }
50
51    /// Extract geometry for a single entity
52    fn extract_geometry(&self, id: EntityId) -> Option<EntityGeometry> {
53        let node = self.model.node(id)?;
54
55        // Get USD mesh data
56        let mesh_value = node.attributes.get(attr::MESH)?;
57        let usd_mesh = UsdMesh::from_value(mesh_value)?;
58
59        // Convert to MeshData
60        let mesh_data = usd_mesh_to_mesh_data(&usd_mesh)?;
61
62        // Get transform (if any)
63        let transform = node
64            .attributes
65            .get(attr::TRANSFORM)
66            .and_then(Transform4x4::from_value)
67            .unwrap_or_default();
68
69        // Get color from presentation attributes or default by type
70        let color = extract_color(node, &self.model);
71
72        // Convert transform to column-major f32 array
73        let transform_array = transform_to_array(&transform);
74
75        Some(EntityGeometry::new(
76            Arc::new(mesh_data),
77            color,
78            transform_array,
79        ))
80    }
81}
82
83impl GeometrySource for IfcxGeometry {
84    fn entities_with_geometry(&self) -> Vec<EntityId> {
85        self.entities_with_geom.clone()
86    }
87
88    fn has_geometry(&self, id: EntityId) -> bool {
89        if let Some(node) = self.model.node(id) {
90            node.attributes.contains_key(attr::MESH)
91        } else {
92            false
93        }
94    }
95
96    fn get_geometry(&self, id: EntityId) -> Option<EntityGeometry> {
97        // Note: In a real implementation we'd use interior mutability for caching
98        // For now, just extract directly
99        self.extract_geometry(id)
100    }
101}
102
103/// Convert USD mesh to MeshData format
104fn usd_mesh_to_mesh_data(usd: &UsdMesh) -> Option<MeshData> {
105    if usd.points.is_empty() {
106        return None;
107    }
108
109    // Get triangulated indices
110    let indices = usd.triangulate();
111    if indices.is_empty() {
112        return None;
113    }
114
115    // Convert points to flat f32 array
116    let mut positions = Vec::with_capacity(usd.points.len() * 3);
117    for p in &usd.points {
118        positions.push(p[0] as f32);
119        positions.push(p[1] as f32);
120        positions.push(p[2] as f32);
121    }
122
123    // Compute or use provided normals
124    let normals = if let Some(ref usd_normals) = usd.normals {
125        // Use provided normals
126        let mut normals = Vec::with_capacity(usd_normals.len() * 3);
127        for n in usd_normals {
128            normals.push(n[0] as f32);
129            normals.push(n[1] as f32);
130            normals.push(n[2] as f32);
131        }
132        normals
133    } else {
134        // Compute normals from triangles
135        compute_normals(&positions, &indices)
136    };
137
138    Some(MeshData {
139        positions,
140        normals,
141        indices,
142    })
143}
144
145/// Compute flat normals for triangles
146fn compute_normals(positions: &[f32], indices: &[u32]) -> Vec<f32> {
147    let vertex_count = positions.len() / 3;
148    let mut normals = vec![0.0f32; vertex_count * 3];
149    let mut counts = vec![0u32; vertex_count];
150
151    // Accumulate face normals for each vertex
152    for tri in indices.chunks(3) {
153        if tri.len() < 3 {
154            continue;
155        }
156
157        let i0 = tri[0] as usize;
158        let i1 = tri[1] as usize;
159        let i2 = tri[2] as usize;
160
161        if i0 * 3 + 2 >= positions.len()
162            || i1 * 3 + 2 >= positions.len()
163            || i2 * 3 + 2 >= positions.len()
164        {
165            continue;
166        }
167
168        // Get vertices
169        let v0 = [
170            positions[i0 * 3],
171            positions[i0 * 3 + 1],
172            positions[i0 * 3 + 2],
173        ];
174        let v1 = [
175            positions[i1 * 3],
176            positions[i1 * 3 + 1],
177            positions[i1 * 3 + 2],
178        ];
179        let v2 = [
180            positions[i2 * 3],
181            positions[i2 * 3 + 1],
182            positions[i2 * 3 + 2],
183        ];
184
185        // Compute edges
186        let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
187        let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
188
189        // Cross product
190        let nx = e1[1] * e2[2] - e1[2] * e2[1];
191        let ny = e1[2] * e2[0] - e1[0] * e2[2];
192        let nz = e1[0] * e2[1] - e1[1] * e2[0];
193
194        // Accumulate to each vertex
195        for &idx in &[i0, i1, i2] {
196            normals[idx * 3] += nx;
197            normals[idx * 3 + 1] += ny;
198            normals[idx * 3 + 2] += nz;
199            counts[idx] += 1;
200        }
201    }
202
203    // Normalize
204    for i in 0..vertex_count {
205        if counts[i] > 0 {
206            let nx = normals[i * 3];
207            let ny = normals[i * 3 + 1];
208            let nz = normals[i * 3 + 2];
209            let len = (nx * nx + ny * ny + nz * nz).sqrt();
210            if len > 1e-6 {
211                normals[i * 3] = nx / len;
212                normals[i * 3 + 1] = ny / len;
213                normals[i * 3 + 2] = nz / len;
214            } else {
215                // Default up normal
216                normals[i * 3] = 0.0;
217                normals[i * 3 + 1] = 1.0;
218                normals[i * 3 + 2] = 0.0;
219            }
220        }
221    }
222
223    normals
224}
225
226/// Extract color from node attributes
227fn extract_color(node: &super::types::ComposedNode, model: &IfcxModel) -> [f32; 4] {
228    // Try diffuse color attribute
229    if let Some(color_val) = node.attributes.get(attr::DIFFUSE_COLOR) {
230        if let Some(arr) = color_val.as_array() {
231            if arr.len() >= 3 {
232                let r = arr[0].as_f64().unwrap_or(0.7) as f32;
233                let g = arr[1].as_f64().unwrap_or(0.7) as f32;
234                let b = arr[2].as_f64().unwrap_or(0.7) as f32;
235
236                // Get opacity
237                let a = node
238                    .attributes
239                    .get(attr::OPACITY)
240                    .and_then(|v| v.as_f64())
241                    .unwrap_or(1.0) as f32;
242
243                return [r, g, b, a];
244            }
245        }
246    }
247
248    // Fall back to default color based on type
249    if let Some(entity) = model
250        .resolver()
251        .get(model.id_for_path(&node.path).unwrap_or_default())
252    {
253        return get_default_color(&entity.ifc_type);
254    }
255
256    // Ultimate fallback
257    [0.7, 0.7, 0.7, 1.0]
258}
259
260/// Convert Transform4x4 to column-major f32 array
261fn transform_to_array(transform: &Transform4x4) -> [f32; 16] {
262    let m = &transform.matrix;
263    // Column-major order for OpenGL/GPU
264    [
265        m[0][0] as f32,
266        m[1][0] as f32,
267        m[2][0] as f32,
268        m[3][0] as f32,
269        m[0][1] as f32,
270        m[1][1] as f32,
271        m[2][1] as f32,
272        m[3][1] as f32,
273        m[0][2] as f32,
274        m[1][2] as f32,
275        m[2][2] as f32,
276        m[3][2] as f32,
277        m[0][3] as f32,
278        m[1][3] as f32,
279        m[2][3] as f32,
280        m[3][3] as f32,
281    ]
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_usd_mesh_conversion() {
290        let usd = UsdMesh {
291            points: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
292            face_vertex_indices: vec![0, 1, 2],
293            face_vertex_counts: None,
294            normals: None,
295        };
296
297        let mesh = usd_mesh_to_mesh_data(&usd).unwrap();
298
299        assert_eq!(mesh.positions.len(), 9); // 3 vertices * 3 components
300        assert_eq!(mesh.indices.len(), 3); // 1 triangle
301        assert_eq!(mesh.normals.len(), 9); // 3 vertices * 3 components
302    }
303
304    #[test]
305    fn test_triangulation() {
306        // Quad face
307        let usd = UsdMesh {
308            points: vec![
309                [0.0, 0.0, 0.0],
310                [1.0, 0.0, 0.0],
311                [1.0, 1.0, 0.0],
312                [0.0, 1.0, 0.0],
313            ],
314            face_vertex_indices: vec![0, 1, 2, 3],
315            face_vertex_counts: Some(vec![4]), // One quad
316            normals: None,
317        };
318
319        let indices = usd.triangulate();
320        assert_eq!(indices.len(), 6); // 2 triangles * 3 vertices
321    }
322}