Skip to main content

ifc_lite_geometry/processors/
tessellated.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//! Tessellated geometry processors - pre-tessellated/polygon meshes.
6//!
7//! Handles IfcTriangulatedFaceSet (explicit triangle meshes) and
8//! IfcPolygonalFaceSet (polygon meshes requiring triangulation).
9
10use crate::{Error, Mesh, Result};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
12
13use crate::router::GeometryProcessor;
14
15/// TriangulatedFaceSet processor (P0)
16/// Handles IfcTriangulatedFaceSet - explicit triangle meshes
17pub struct TriangulatedFaceSetProcessor;
18
19impl TriangulatedFaceSetProcessor {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl GeometryProcessor for TriangulatedFaceSetProcessor {
26    #[inline]
27    fn process(
28        &self,
29        entity: &DecodedEntity,
30        decoder: &mut EntityDecoder,
31        _schema: &IfcSchema,
32    ) -> Result<Mesh> {
33        // IfcTriangulatedFaceSet attributes:
34        // 0: Coordinates (IfcCartesianPointList3D)
35        // 1: Normals (optional)
36        // 2: Closed (optional)
37        // 3: CoordIndex (list of list of IfcPositiveInteger)
38
39        // Get coordinate entity reference
40        let coords_attr = entity.get(0).ok_or_else(|| {
41            Error::geometry("TriangulatedFaceSet missing Coordinates".to_string())
42        })?;
43
44        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
45            Error::geometry("Expected entity reference for Coordinates".to_string())
46        })?;
47
48        // FAST PATH: Try direct parsing of raw bytes (3-5x faster)
49        // This bypasses Token/AttributeValue allocations entirely
50        use ifc_lite_core::{extract_coordinate_list_from_entity, parse_indices_direct};
51
52        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
53            // Fast path: parse coordinates directly from raw bytes
54            // Use extract_coordinate_list_from_entity to skip entity header (#N=IFCTYPE...)
55            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
56        } else {
57            // Fallback path: use standard decoding
58            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
59
60            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
61                Error::geometry("CartesianPointList3D missing CoordList".to_string())
62            })?;
63
64            let coord_list = coord_list_attr
65                .as_list()
66                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
67
68            use ifc_lite_core::AttributeValue;
69            AttributeValue::parse_coordinate_list_3d(coord_list)
70        };
71
72        // Get face indices - try fast path first
73        let indices_attr = entity
74            .get(3)
75            .ok_or_else(|| Error::geometry("TriangulatedFaceSet missing CoordIndex".to_string()))?;
76
77        // For indices, we need to extract from the main entity's raw bytes
78        // Fast path: parse directly if we can get the raw CoordIndex section
79        let indices = if let Some(raw_entity_bytes) = decoder.get_raw_bytes(entity.id) {
80            // Find the CoordIndex attribute (4th attribute, index 3)
81            // and parse directly
82            if let Some(coord_index_bytes) = super::extract_coord_index_bytes(raw_entity_bytes) {
83                parse_indices_direct(coord_index_bytes)
84            } else {
85                // Fallback to standard parsing
86                let face_list = indices_attr
87                    .as_list()
88                    .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
89                use ifc_lite_core::AttributeValue;
90                AttributeValue::parse_index_list(face_list)
91            }
92        } else {
93            let face_list = indices_attr
94                .as_list()
95                .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
96            use ifc_lite_core::AttributeValue;
97            AttributeValue::parse_index_list(face_list)
98        };
99
100        // Create mesh (normals will be computed later)
101        Ok(Mesh {
102            positions,
103            normals: Vec::new(),
104            indices,
105        })
106    }
107
108    fn supported_types(&self) -> Vec<IfcType> {
109        vec![IfcType::IfcTriangulatedFaceSet]
110    }
111}
112
113impl Default for TriangulatedFaceSetProcessor {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119/// Handles IfcPolygonalFaceSet - explicit polygon meshes that need triangulation
120/// Unlike IfcTriangulatedFaceSet, faces can be arbitrary polygons (not just triangles)
121pub struct PolygonalFaceSetProcessor;
122
123impl PolygonalFaceSetProcessor {
124    pub fn new() -> Self {
125        Self
126    }
127
128    /// Triangulate a polygon using ear-clipping algorithm (earcutr)
129    /// This works correctly for both convex and concave polygons
130    /// IFC indices are 1-based, so we subtract 1 to get 0-based indices
131    /// positions is flattened [x0, y0, z0, x1, y1, z1, ...]
132    fn triangulate_polygon(
133        face_indices: &[u32],
134        positions: &[f32],
135        output: &mut Vec<u32>,
136    ) {
137        if face_indices.len() < 3 {
138            return;
139        }
140
141        // For triangles, no triangulation needed
142        if face_indices.len() == 3 {
143            output.push(face_indices[0] - 1);
144            output.push(face_indices[1] - 1);
145            output.push(face_indices[2] - 1);
146            return;
147        }
148
149        // For quads and simple cases, use fan triangulation (fast path)
150        if face_indices.len() == 4 {
151            let first = face_indices[0] - 1;
152            output.push(first);
153            output.push(face_indices[1] - 1);
154            output.push(face_indices[2] - 1);
155            output.push(first);
156            output.push(face_indices[2] - 1);
157            output.push(face_indices[3] - 1);
158            return;
159        }
160
161        // Helper to get 3D position from flattened array
162        let get_pos = |idx: u32| -> Option<(f32, f32, f32)> {
163            let base = ((idx - 1) * 3) as usize;
164            if base + 2 < positions.len() {
165                Some((positions[base], positions[base + 1], positions[base + 2]))
166            } else {
167                None
168            }
169        };
170
171        // For complex polygons (5+ vertices), use ear-clipping triangulation
172        // This handles concave polygons correctly (like opening cutouts)
173
174        // Extract 2D coordinates by projecting to best-fit plane
175        // Find dominant normal direction to choose projection plane
176        let mut sum_x = 0.0f64;
177        let mut sum_y = 0.0f64;
178        let mut sum_z = 0.0f64;
179
180        // Calculate centroid-based normal approximation using Newell's method
181        for i in 0..face_indices.len() {
182            let v0 = match get_pos(face_indices[i]) {
183                Some(p) => p,
184                None => {
185                    // Fallback to fan triangulation if indices are invalid
186                    let first = face_indices[0] - 1;
187                    for j in 1..face_indices.len() - 1 {
188                        output.push(first);
189                        output.push(face_indices[j] - 1);
190                        output.push(face_indices[j + 1] - 1);
191                    }
192                    return;
193                }
194            };
195            let v1 = match get_pos(face_indices[(i + 1) % face_indices.len()]) {
196                Some(p) => p,
197                None => {
198                    let first = face_indices[0] - 1;
199                    for j in 1..face_indices.len() - 1 {
200                        output.push(first);
201                        output.push(face_indices[j] - 1);
202                        output.push(face_indices[j + 1] - 1);
203                    }
204                    return;
205                }
206            };
207
208            sum_x += (v0.1 - v1.1) as f64 * (v0.2 + v1.2) as f64;
209            sum_y += (v0.2 - v1.2) as f64 * (v0.0 + v1.0) as f64;
210            sum_z += (v0.0 - v1.0) as f64 * (v0.1 + v1.1) as f64;
211        }
212
213        // Choose projection plane based on dominant axis
214        let abs_x = sum_x.abs();
215        let abs_y = sum_y.abs();
216        let abs_z = sum_z.abs();
217
218        // Project 3D points to 2D for triangulation
219        let mut coords_2d: Vec<f64> = Vec::with_capacity(face_indices.len() * 2);
220
221        for &idx in face_indices {
222            let p = match get_pos(idx) {
223                Some(pos) => pos,
224                None => {
225                    // Fallback to fan triangulation
226                    let first = face_indices[0] - 1;
227                    for j in 1..face_indices.len() - 1 {
228                        output.push(first);
229                        output.push(face_indices[j] - 1);
230                        output.push(face_indices[j + 1] - 1);
231                    }
232                    return;
233                }
234            };
235
236            // Project to 2D based on dominant normal axis
237            if abs_z >= abs_x && abs_z >= abs_y {
238                // XY plane (Z is dominant)
239                coords_2d.push(p.0 as f64);
240                coords_2d.push(p.1 as f64);
241            } else if abs_y >= abs_x {
242                // XZ plane (Y is dominant)
243                coords_2d.push(p.0 as f64);
244                coords_2d.push(p.2 as f64);
245            } else {
246                // YZ plane (X is dominant)
247                coords_2d.push(p.1 as f64);
248                coords_2d.push(p.2 as f64);
249            }
250        }
251
252        // Run ear-clipping triangulation
253        let hole_indices: Vec<usize> = vec![]; // No holes for simple faces
254        match earcutr::earcut(&coords_2d, &hole_indices, 2) {
255            Ok(tri_indices) => {
256                // Map local triangle indices back to original face indices
257                for tri_idx in tri_indices {
258                    if tri_idx < face_indices.len() {
259                        output.push(face_indices[tri_idx] - 1);
260                    }
261                }
262            }
263            Err(_) => {
264                // Fallback to fan triangulation if ear-clipping fails
265                let first = face_indices[0] - 1;
266                for i in 1..face_indices.len() - 1 {
267                    output.push(first);
268                    output.push(face_indices[i] - 1);
269                    output.push(face_indices[i + 1] - 1);
270                }
271            }
272        }
273    }
274}
275
276impl GeometryProcessor for PolygonalFaceSetProcessor {
277    fn process(
278        &self,
279        entity: &DecodedEntity,
280        decoder: &mut EntityDecoder,
281        _schema: &IfcSchema,
282    ) -> Result<Mesh> {
283        // IfcPolygonalFaceSet attributes:
284        // 0: Coordinates (IfcCartesianPointList3D)
285        // 1: Closed (optional BOOLEAN)
286        // 2: Faces (LIST of IfcIndexedPolygonalFace)
287        // 3: PnIndex (optional - point index remapping)
288
289        // Get coordinate entity reference
290        let coords_attr = entity.get(0).ok_or_else(|| {
291            Error::geometry("PolygonalFaceSet missing Coordinates".to_string())
292        })?;
293
294        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
295            Error::geometry("Expected entity reference for Coordinates".to_string())
296        })?;
297
298        // Parse coordinates - try fast path first
299        use ifc_lite_core::extract_coordinate_list_from_entity;
300
301        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
302            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
303        } else {
304            // Fallback path
305            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
306            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
307                Error::geometry("CartesianPointList3D missing CoordList".to_string())
308            })?;
309            let coord_list = coord_list_attr
310                .as_list()
311                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
312            use ifc_lite_core::AttributeValue;
313            AttributeValue::parse_coordinate_list_3d(coord_list)
314        };
315
316        if positions.is_empty() {
317            return Ok(Mesh::new());
318        }
319
320        // Get faces list (attribute 2)
321        let faces_attr = entity.get(2).ok_or_else(|| {
322            Error::geometry("PolygonalFaceSet missing Faces".to_string())
323        })?;
324
325        let face_refs = faces_attr
326            .as_list()
327            .ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
328
329        // Pre-allocate indices - estimate 2 triangles per face average
330        let mut indices = Vec::with_capacity(face_refs.len() * 6);
331
332        // Process each face
333        for face_ref in face_refs {
334            let face_id = face_ref.as_entity_ref().ok_or_else(|| {
335                Error::geometry("Expected entity reference for face".to_string())
336            })?;
337
338            let face_entity = decoder.decode_by_id(face_id)?;
339
340            // IfcIndexedPolygonalFace has CoordIndex at attribute 0
341            // IfcIndexedPolygonalFaceWithVoids has CoordIndex at 0 and InnerCoordIndices at 1
342            let coord_index_attr = face_entity.get(0).ok_or_else(|| {
343                Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
344            })?;
345
346            let coord_indices = coord_index_attr
347                .as_list()
348                .ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
349
350            // Parse face indices (1-based in IFC)
351            let face_indices: Vec<u32> = coord_indices
352                .iter()
353                .filter_map(|v| v.as_int().map(|i| i as u32))
354                .collect();
355
356            // Triangulate the polygon (using ear-clipping for complex polygons)
357            Self::triangulate_polygon(&face_indices, &positions, &mut indices);
358        }
359
360        Ok(Mesh {
361            positions,
362            normals: Vec::new(), // Will be computed later
363            indices,
364        })
365    }
366
367    fn supported_types(&self) -> Vec<IfcType> {
368        vec![IfcType::IfcPolygonalFaceSet]
369    }
370}
371
372impl Default for PolygonalFaceSetProcessor {
373    fn default() -> Self {
374        Self::new()
375    }
376}