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::{AttributeValue, 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        let mut mesh = Mesh {
102            positions,
103            normals: Vec::new(),
104            indices,
105            rtc_applied: false,
106        };
107        // Validate: IFC files (especially Revit exports) may have indices beyond vertex count
108        mesh.validate_indices();
109        Ok(mesh)
110    }
111
112    fn supported_types(&self) -> Vec<IfcType> {
113        vec![IfcType::IfcTriangulatedFaceSet]
114    }
115}
116
117impl Default for TriangulatedFaceSetProcessor {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123/// Handles IfcPolygonalFaceSet - explicit polygon meshes that need triangulation
124/// Unlike IfcTriangulatedFaceSet, faces can be arbitrary polygons (not just triangles)
125pub struct PolygonalFaceSetProcessor;
126
127impl PolygonalFaceSetProcessor {
128    pub fn new() -> Self {
129        Self
130    }
131
132    #[inline]
133    fn parse_index_loop(indices: &[AttributeValue], pn_index: Option<&[u32]>) -> Vec<u32> {
134        indices
135            .iter()
136            .filter_map(|value| {
137                let idx = value.as_int()?;
138                if idx <= 0 {
139                    return None;
140                }
141                let idx = idx as usize;
142
143                if let Some(remap) = pn_index {
144                    remap.get(idx - 1).copied().filter(|mapped| *mapped > 0)
145                } else {
146                    Some(idx as u32)
147                }
148            })
149            .collect()
150    }
151
152    /// Triangulate a polygon (optionally with holes) using ear-clipping (earcutr)
153    /// This works correctly for both convex and concave polygons
154    /// IFC indices are 1-based, so we subtract 1 to get 0-based indices
155    /// positions is flattened [x0, y0, z0, x1, y1, z1, ...]
156    fn triangulate_polygon(
157        outer_indices: &[u32],
158        inner_indices: &[Vec<u32>],
159        positions: &[f32],
160        output: &mut Vec<u32>,
161    ) {
162        if outer_indices.len() < 3 {
163            return;
164        }
165
166        // Helper to get 3D position from flattened array
167        let get_pos = |idx: u32| -> Option<(f32, f32, f32)> {
168            if idx == 0 {
169                return None;
170            }
171            let base = ((idx - 1) * 3) as usize;
172            if base + 2 < positions.len() {
173                Some((positions[base], positions[base + 1], positions[base + 2]))
174            } else {
175                None
176            }
177        };
178
179        // Guard: empty outer_indices would panic on any [0] access below
180        if outer_indices.is_empty() {
181            return;
182        }
183
184        // For complex polygons (5+ vertices), use ear-clipping triangulation
185        // This handles concave polygons correctly (like opening cutouts)
186
187        // Extract 2D coordinates by projecting to best-fit plane
188        // Find dominant normal direction to choose projection plane
189        let mut sum_x = 0.0f64;
190        let mut sum_y = 0.0f64;
191        let mut sum_z = 0.0f64;
192
193        // Calculate centroid-based normal approximation using Newell's method
194        for i in 0..outer_indices.len() {
195            let v0 = match get_pos(outer_indices[i]) {
196                Some(p) => p,
197                None => {
198                    // Invalid vertex index — skip this polygon entirely.
199                    // We cannot safely fan-triangulate with unresolvable vertices.
200                    return;
201                }
202            };
203            let v1 = match get_pos(outer_indices[(i + 1) % outer_indices.len()]) {
204                Some(p) => p,
205                None => {
206                    return;
207                }
208            };
209
210            sum_x += (v0.1 - v1.1) as f64 * (v0.2 + v1.2) as f64;
211            sum_y += (v0.2 - v1.2) as f64 * (v0.0 + v1.0) as f64;
212            sum_z += (v0.0 - v1.0) as f64 * (v0.1 + v1.1) as f64;
213        }
214        let expected_normal = (sum_x, sum_y, sum_z);
215
216        let mut push_oriented_triangle = |a: u32, b: u32, c: u32| {
217            if a == 0 || b == 0 || c == 0 {
218                return;
219            }
220            let i0 = a - 1;
221            let mut i1 = b - 1;
222            let mut i2 = c - 1;
223
224            if expected_normal.0.abs() + expected_normal.1.abs() + expected_normal.2.abs() > 1e-12 {
225                if let (Some(p0), Some(p1), Some(p2)) = (get_pos(a), get_pos(b), get_pos(c)) {
226                    let e1 = (
227                        (p1.0 - p0.0) as f64,
228                        (p1.1 - p0.1) as f64,
229                        (p1.2 - p0.2) as f64,
230                    );
231                    let e2 = (
232                        (p2.0 - p0.0) as f64,
233                        (p2.1 - p0.1) as f64,
234                        (p2.2 - p0.2) as f64,
235                    );
236                    let tri_normal = (
237                        e1.1 * e2.2 - e1.2 * e2.1,
238                        e1.2 * e2.0 - e1.0 * e2.2,
239                        e1.0 * e2.1 - e1.1 * e2.0,
240                    );
241                    let dot = tri_normal.0 * expected_normal.0
242                        + tri_normal.1 * expected_normal.1
243                        + tri_normal.2 * expected_normal.2;
244                    if dot < 0.0 {
245                        std::mem::swap(&mut i1, &mut i2);
246                    }
247                }
248            }
249
250            output.push(i0);
251            output.push(i1);
252            output.push(i2);
253        };
254
255        // For triangles, no triangulation needed (but still enforce orientation)
256        if inner_indices.is_empty() && outer_indices.len() == 3 {
257            push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
258            return;
259        }
260
261        // For quads, use fan triangulation with orientation correction
262        if inner_indices.is_empty() && outer_indices.len() == 4 {
263            push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
264            push_oriented_triangle(outer_indices[0], outer_indices[2], outer_indices[3]);
265            return;
266        }
267
268        // Choose projection plane based on dominant axis
269        let abs_x = sum_x.abs();
270        let abs_y = sum_y.abs();
271        let abs_z = sum_z.abs();
272
273        let valid_holes: Vec<&[u32]> = inner_indices
274            .iter()
275            .filter(|loop_indices| loop_indices.len() >= 3)
276            .map(|loop_indices| loop_indices.as_slice())
277            .collect();
278
279        // Flatten all loops for earcut (outer ring first, then holes)
280        let total_vertices = outer_indices.len()
281            + valid_holes
282                .iter()
283                .map(|loop_indices| loop_indices.len())
284                .sum::<usize>();
285        let mut coords_2d: Vec<f64> = Vec::with_capacity(total_vertices * 2);
286        let mut flattened_indices: Vec<u32> = Vec::with_capacity(total_vertices);
287        let mut hole_starts: Vec<usize> = Vec::with_capacity(valid_holes.len());
288
289        for &idx in outer_indices {
290            let Some(p) = get_pos(idx) else {
291                // Invalid vertex — skip polygon (fan-triangulate would include bad vertices)
292                return;
293            };
294            flattened_indices.push(idx);
295
296            // Project to 2D based on dominant normal axis
297            if abs_z >= abs_x && abs_z >= abs_y {
298                // XY plane (Z is dominant)
299                coords_2d.push(p.0 as f64);
300                coords_2d.push(p.1 as f64);
301            } else if abs_y >= abs_x {
302                // XZ plane (Y is dominant)
303                coords_2d.push(p.0 as f64);
304                coords_2d.push(p.2 as f64);
305            } else {
306                // YZ plane (X is dominant)
307                coords_2d.push(p.1 as f64);
308                coords_2d.push(p.2 as f64);
309            }
310        }
311
312        for hole in valid_holes {
313            hole_starts.push(flattened_indices.len());
314            for &idx in hole {
315                let Some(p) = get_pos(idx) else {
316                    // Invalid hole vertex — skip polygon
317                    return;
318                };
319                flattened_indices.push(idx);
320
321                // Project to 2D based on dominant normal axis
322                if abs_z >= abs_x && abs_z >= abs_y {
323                    // XY plane (Z is dominant)
324                    coords_2d.push(p.0 as f64);
325                    coords_2d.push(p.1 as f64);
326                } else if abs_y >= abs_x {
327                    // XZ plane (Y is dominant)
328                    coords_2d.push(p.0 as f64);
329                    coords_2d.push(p.2 as f64);
330                } else {
331                    // YZ plane (X is dominant)
332                    coords_2d.push(p.1 as f64);
333                    coords_2d.push(p.2 as f64);
334                }
335            }
336        }
337
338        if flattened_indices.len() < 3 {
339            return;
340        }
341
342        // Run ear-clipping triangulation
343        match earcutr::earcut(&coords_2d, &hole_starts, 2) {
344            Ok(tri_indices) => {
345                for tri in tri_indices.chunks(3) {
346                    if tri.len() != 3
347                        || tri[0] >= flattened_indices.len()
348                        || tri[1] >= flattened_indices.len()
349                        || tri[2] >= flattened_indices.len()
350                    {
351                        continue;
352                    }
353                    push_oriented_triangle(
354                        flattened_indices[tri[0]],
355                        flattened_indices[tri[1]],
356                        flattened_indices[tri[2]],
357                    );
358                }
359            }
360            Err(_) => {
361                // Fallback to fan triangulation on the outer loop
362                let first = outer_indices[0];
363                for i in 1..outer_indices.len() - 1 {
364                    push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
365                }
366            }
367        }
368    }
369
370    #[inline]
371    fn parse_face_inner_indices(
372        face_entity: &DecodedEntity,
373        pn_index: Option<&[u32]>,
374    ) -> Vec<Vec<u32>> {
375        if face_entity.ifc_type != IfcType::IfcIndexedPolygonalFaceWithVoids {
376            return Vec::new();
377        }
378
379        let Some(inner_attr) = face_entity.get(1).and_then(|a| a.as_list()) else {
380            return Vec::new();
381        };
382
383        let mut result = Vec::with_capacity(inner_attr.len());
384        for loop_attr in inner_attr {
385            let Some(loop_values) = loop_attr.as_list() else {
386                continue;
387            };
388            let parsed = Self::parse_index_loop(loop_values, pn_index);
389            if parsed.len() >= 3 {
390                result.push(parsed);
391            }
392        }
393
394        result
395    }
396
397    #[inline]
398    fn orient_closed_shell_outward(positions: &[f32], indices: &mut [u32]) {
399        if indices.len() < 3 || positions.len() < 9 {
400            return;
401        }
402
403        let vertex_count = positions.len() / 3;
404        if vertex_count == 0 {
405            return;
406        }
407
408        // Mesh centroid
409        let mut cx = 0.0f64;
410        let mut cy = 0.0f64;
411        let mut cz = 0.0f64;
412        for p in positions.chunks_exact(3) {
413            cx += p[0] as f64;
414            cy += p[1] as f64;
415            cz += p[2] as f64;
416        }
417        let inv_n = 1.0 / vertex_count as f64;
418        cx *= inv_n;
419        cy *= inv_n;
420        cz *= inv_n;
421
422        let mut sign_accum = 0.0f64;
423        for tri in indices.chunks_exact(3) {
424            let i0 = tri[0] as usize;
425            let i1 = tri[1] as usize;
426            let i2 = tri[2] as usize;
427            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
428                continue;
429            }
430
431            let p0 = (
432                positions[i0 * 3] as f64,
433                positions[i0 * 3 + 1] as f64,
434                positions[i0 * 3 + 2] as f64,
435            );
436            let p1 = (
437                positions[i1 * 3] as f64,
438                positions[i1 * 3 + 1] as f64,
439                positions[i1 * 3 + 2] as f64,
440            );
441            let p2 = (
442                positions[i2 * 3] as f64,
443                positions[i2 * 3 + 1] as f64,
444                positions[i2 * 3 + 2] as f64,
445            );
446
447            let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
448            let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
449            let n = (
450                e1.1 * e2.2 - e1.2 * e2.1,
451                e1.2 * e2.0 - e1.0 * e2.2,
452                e1.0 * e2.1 - e1.1 * e2.0,
453            );
454
455            let tc = (
456                (p0.0 + p1.0 + p2.0) / 3.0,
457                (p0.1 + p1.1 + p2.1) / 3.0,
458                (p0.2 + p1.2 + p2.2) / 3.0,
459            );
460            let out = (tc.0 - cx, tc.1 - cy, tc.2 - cz);
461            sign_accum += n.0 * out.0 + n.1 * out.1 + n.2 * out.2;
462        }
463
464        // If most triangles point inward, flip all winding.
465        if sign_accum < 0.0 {
466            for tri in indices.chunks_exact_mut(3) {
467                tri.swap(1, 2);
468            }
469        }
470    }
471
472    #[inline]
473    fn build_flat_shaded_mesh(positions: &[f32], indices: &[u32]) -> Mesh {
474        let mut flat_positions: Vec<f32> = Vec::with_capacity(indices.len() * 3);
475        let mut flat_normals: Vec<f32> = Vec::with_capacity(indices.len() * 3);
476        let mut flat_indices: Vec<u32> = Vec::with_capacity(indices.len());
477
478        let vertex_count = positions.len() / 3;
479        let mut next_index: u32 = 0;
480
481        for tri in indices.chunks_exact(3) {
482            let i0 = tri[0] as usize;
483            let i1 = tri[1] as usize;
484            let i2 = tri[2] as usize;
485            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
486                continue;
487            }
488
489            let p0 = (
490                positions[i0 * 3] as f64,
491                positions[i0 * 3 + 1] as f64,
492                positions[i0 * 3 + 2] as f64,
493            );
494            let p1 = (
495                positions[i1 * 3] as f64,
496                positions[i1 * 3 + 1] as f64,
497                positions[i1 * 3 + 2] as f64,
498            );
499            let p2 = (
500                positions[i2 * 3] as f64,
501                positions[i2 * 3 + 1] as f64,
502                positions[i2 * 3 + 2] as f64,
503            );
504
505            let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
506            let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
507            let nx = e1.1 * e2.2 - e1.2 * e2.1;
508            let ny = e1.2 * e2.0 - e1.0 * e2.2;
509            let nz = e1.0 * e2.1 - e1.1 * e2.0;
510            let len = (nx * nx + ny * ny + nz * nz).sqrt();
511            let (nx, ny, nz) = if len > 1e-12 {
512                (nx / len, ny / len, nz / len)
513            } else {
514                (0.0, 0.0, 1.0)
515            };
516
517            for &idx in &[i0, i1, i2] {
518                flat_positions.push(positions[idx * 3]);
519                flat_positions.push(positions[idx * 3 + 1]);
520                flat_positions.push(positions[idx * 3 + 2]);
521                flat_normals.push(nx as f32);
522                flat_normals.push(ny as f32);
523                flat_normals.push(nz as f32);
524                flat_indices.push(next_index);
525                next_index += 1;
526            }
527        }
528
529        Mesh {
530            positions: flat_positions,
531            normals: flat_normals,
532            indices: flat_indices,
533            rtc_applied: false,
534        }
535    }
536}
537
538impl GeometryProcessor for PolygonalFaceSetProcessor {
539    fn process(
540        &self,
541        entity: &DecodedEntity,
542        decoder: &mut EntityDecoder,
543        _schema: &IfcSchema,
544    ) -> Result<Mesh> {
545        // IfcPolygonalFaceSet attributes:
546        // 0: Coordinates (IfcCartesianPointList3D)
547        // 1: Closed (optional BOOLEAN)
548        // 2: Faces (LIST of IfcIndexedPolygonalFace)
549        // 3: PnIndex (optional - point index remapping)
550
551        // Get coordinate entity reference
552        let coords_attr = entity
553            .get(0)
554            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Coordinates".to_string()))?;
555
556        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
557            Error::geometry("Expected entity reference for Coordinates".to_string())
558        })?;
559
560        // Parse coordinates - try fast path first
561        use ifc_lite_core::extract_coordinate_list_from_entity;
562
563        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
564            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
565        } else {
566            // Fallback path
567            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
568            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
569                Error::geometry("CartesianPointList3D missing CoordList".to_string())
570            })?;
571            let coord_list = coord_list_attr
572                .as_list()
573                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
574            AttributeValue::parse_coordinate_list_3d(coord_list)
575        };
576
577        if positions.is_empty() {
578            return Ok(Mesh::new());
579        }
580
581        // Get faces list (attribute 2)
582        let faces_attr = entity
583            .get(2)
584            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Faces".to_string()))?;
585
586        let face_refs = faces_attr
587            .as_list()
588            .ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
589
590        // Optional point remapping list for IfcPolygonalFaceSet.
591        // CoordIndex values refer to this list when present.
592        let pn_index = entity.get(3).and_then(|attr| attr.as_list()).map(|list| {
593            list.iter()
594                .filter_map(|value| value.as_int())
595                .filter(|v| *v > 0)
596                .map(|v| v as u32)
597                .collect::<Vec<u32>>()
598        });
599
600        // Pre-allocate indices - estimate 2 triangles per face average
601        let mut indices = Vec::with_capacity(face_refs.len() * 6);
602
603        // Process each face
604        for face_ref in face_refs {
605            let face_id = face_ref
606                .as_entity_ref()
607                .ok_or_else(|| Error::geometry("Expected entity reference for face".to_string()))?;
608
609            let face_entity = decoder.decode_by_id(face_id)?;
610
611            // IfcIndexedPolygonalFace has CoordIndex at attribute 0
612            // IfcIndexedPolygonalFaceWithVoids has CoordIndex at 0 and InnerCoordIndices at 1
613            let coord_index_attr = face_entity.get(0).ok_or_else(|| {
614                Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
615            })?;
616
617            let coord_indices = coord_index_attr
618                .as_list()
619                .ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
620
621            // Parse face indices (1-based in IFC), with optional PnIndex remapping.
622            let face_indices = Self::parse_index_loop(coord_indices, pn_index.as_deref());
623            if face_indices.len() < 3 {
624                continue;
625            }
626
627            // Parse optional inner loops for IfcIndexedPolygonalFaceWithVoids.
628            let inner_indices = Self::parse_face_inner_indices(&face_entity, pn_index.as_deref());
629
630            // Triangulate the polygon face (including holes when present).
631            Self::triangulate_polygon(&face_indices, &inner_indices, &positions, &mut indices);
632        }
633
634        // Closed shells from some exporters may be consistently inward.
635        // Flip globally to outward winding when needed.
636        let is_closed = entity
637            .get(1)
638            .and_then(|a| a.as_enum())
639            .map(|v| v == "T")
640            .unwrap_or(false);
641        if is_closed {
642            Self::orient_closed_shell_outward(&positions, &mut indices);
643        }
644
645        Ok(Self::build_flat_shaded_mesh(&positions, &indices))
646    }
647
648    fn supported_types(&self) -> Vec<IfcType> {
649        vec![IfcType::IfcPolygonalFaceSet]
650    }
651}
652
653impl Default for PolygonalFaceSetProcessor {
654    fn default() -> Self {
655        Self::new()
656    }
657}