ifc_lite_geometry/processors/tessellated/
polygonal.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
5use crate::{Error, Mesh, Result, TessellationQuality};
6use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcSchema, IfcType};
7
8use crate::router::GeometryProcessor;
9
10/// Handles IfcPolygonalFaceSet - explicit polygon meshes that need triangulation
11/// Unlike IfcTriangulatedFaceSet, faces can be arbitrary polygons (not just triangles)
12pub struct PolygonalFaceSetProcessor;
13
14impl PolygonalFaceSetProcessor {
15    pub fn new() -> Self {
16        Self
17    }
18
19    #[inline]
20    fn parse_index_loop(indices: &[AttributeValue], pn_index: Option<&[u32]>) -> Vec<u32> {
21        indices
22            .iter()
23            .filter_map(|value| {
24                let idx = value.as_int()?;
25                if idx <= 0 {
26                    return None;
27                }
28                let idx = idx as usize;
29
30                if let Some(remap) = pn_index {
31                    remap.get(idx - 1).copied().filter(|mapped| *mapped > 0)
32                } else {
33                    Some(idx as u32)
34                }
35            })
36            .collect()
37    }
38
39    #[inline]
40    fn parse_face_inner_indices(
41        face_entity: &DecodedEntity,
42        pn_index: Option<&[u32]>,
43    ) -> Vec<Vec<u32>> {
44        if face_entity.ifc_type != IfcType::IfcIndexedPolygonalFaceWithVoids {
45            return Vec::new();
46        }
47
48        let Some(inner_attr) = face_entity.get(1).and_then(|a| a.as_list()) else {
49            return Vec::new();
50        };
51
52        let mut result = Vec::with_capacity(inner_attr.len());
53        for loop_attr in inner_attr {
54            let Some(loop_values) = loop_attr.as_list() else {
55                continue;
56            };
57            let parsed = Self::parse_index_loop(loop_values, pn_index);
58            if parsed.len() >= 3 {
59                result.push(parsed);
60            }
61        }
62
63        result
64    }
65
66    /// Mesh the face set; `rtc_file_units` rebases before f32 narrowing
67    /// (see `read_point_list`), so triangulation and orientation also run on
68    /// the rebased, collapse-free positions.
69    fn process_rebased(
70        entity: &DecodedEntity,
71        decoder: &mut EntityDecoder,
72        rtc_file_units: Option<(f64, f64, f64)>,
73    ) -> Result<Mesh> {
74        // IfcPolygonalFaceSet attributes:
75        // 0: Coordinates (IfcCartesianPointList3D)
76        // 1: Closed (optional BOOLEAN)
77        // 2: Faces (LIST of IfcIndexedPolygonalFace)
78        // 3: PnIndex (optional - point index remapping)
79
80        let coord_entity_id = entity
81            .get(0)
82            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Coordinates".to_string()))?
83            .as_entity_ref()
84            .ok_or_else(|| {
85                Error::geometry("Expected entity reference for Coordinates".to_string())
86            })?;
87        let positions = super::read_point_list(decoder, coord_entity_id, rtc_file_units)?;
88
89        if positions.is_empty() {
90            return Ok(Mesh::new());
91        }
92
93        // Get faces list (attribute 2)
94        let faces_attr = entity
95            .get(2)
96            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Faces".to_string()))?;
97
98        let face_refs = faces_attr
99            .as_list()
100            .ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
101
102        // Optional point remapping list for IfcPolygonalFaceSet.
103        // CoordIndex values refer to this list when present.
104        let pn_index = entity.get(3).and_then(|attr| attr.as_list()).map(|list| {
105            list.iter()
106                .filter_map(|value| value.as_int())
107                .filter(|v| *v > 0)
108                .map(|v| v as u32)
109                .collect::<Vec<u32>>()
110        });
111
112        // Pre-allocate indices - estimate 2 triangles per face average
113        let mut indices = Vec::with_capacity(face_refs.len() * 6);
114
115        // Process each face
116        for face_ref in face_refs {
117            let face_id = face_ref
118                .as_entity_ref()
119                .ok_or_else(|| Error::geometry("Expected entity reference for face".to_string()))?;
120
121            let face_entity = decoder.decode_by_id(face_id)?;
122
123            // IfcIndexedPolygonalFace has CoordIndex at attribute 0
124            // IfcIndexedPolygonalFaceWithVoids has CoordIndex at 0 and InnerCoordIndices at 1
125            let coord_index_attr = face_entity.get(0).ok_or_else(|| {
126                Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
127            })?;
128
129            let coord_indices = coord_index_attr
130                .as_list()
131                .ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
132
133            // Parse face indices (1-based in IFC), with optional PnIndex remapping.
134            let face_indices = Self::parse_index_loop(coord_indices, pn_index.as_deref());
135            if face_indices.len() < 3 {
136                continue;
137            }
138
139            // Parse optional inner loops for IfcIndexedPolygonalFaceWithVoids.
140            let inner_indices = Self::parse_face_inner_indices(&face_entity, pn_index.as_deref());
141
142            // Triangulate the polygon face (including holes when present).
143            Self::triangulate_polygon(&face_indices, &inner_indices, &positions, &mut indices);
144        }
145
146        // Closed shells from some exporters may be consistently inward.
147        // Flip globally to outward winding when needed.
148        let is_closed = entity
149            .get(1)
150            .and_then(|a| a.as_enum())
151            .map(|v| v == "T")
152            .unwrap_or(false);
153        if is_closed {
154            Self::orient_closed_shell_outward(&positions, &mut indices);
155        }
156
157        let mut mesh = Self::build_flat_shaded_mesh(&positions, &indices);
158        mesh.rtc_applied = rtc_file_units.is_some();
159        Ok(mesh)
160    }
161}
162
163impl GeometryProcessor for PolygonalFaceSetProcessor {
164    fn process(
165        &self,
166        entity: &DecodedEntity,
167        decoder: &mut EntityDecoder,
168        _schema: &IfcSchema,
169        _quality: TessellationQuality,
170    ) -> Result<Mesh> {
171        Self::process_rebased(entity, decoder, None)
172    }
173
174    fn process_in_rtc_frame(
175        &self,
176        entity: &DecodedEntity,
177        decoder: &mut EntityDecoder,
178        _schema: &IfcSchema,
179        _quality: TessellationQuality,
180        rtc_file_units: (f64, f64, f64),
181    ) -> Option<Result<Mesh>> {
182        Some(Self::process_rebased(entity, decoder, Some(rtc_file_units)))
183    }
184
185    fn supported_types(&self) -> Vec<IfcType> {
186        vec![IfcType::IfcPolygonalFaceSet]
187    }
188}
189
190impl Default for PolygonalFaceSetProcessor {
191    fn default() -> Self {
192        Self::new()
193    }
194}