Skip to main content

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
67impl GeometryProcessor for PolygonalFaceSetProcessor {
68    fn process(
69        &self,
70        entity: &DecodedEntity,
71        decoder: &mut EntityDecoder,
72        _schema: &IfcSchema,
73        _quality: TessellationQuality,
74    ) -> Result<Mesh> {
75        // IfcPolygonalFaceSet attributes:
76        // 0: Coordinates (IfcCartesianPointList3D)
77        // 1: Closed (optional BOOLEAN)
78        // 2: Faces (LIST of IfcIndexedPolygonalFace)
79        // 3: PnIndex (optional - point index remapping)
80
81        // Get coordinate entity reference
82        let coords_attr = entity
83            .get(0)
84            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Coordinates".to_string()))?;
85
86        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
87            Error::geometry("Expected entity reference for Coordinates".to_string())
88        })?;
89
90        // Parse coordinates - try fast path first
91        use ifc_lite_core::extract_coordinate_list_from_entity;
92
93        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
94            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
95        } else {
96            // Fallback path
97            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
98            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
99                Error::geometry("CartesianPointList3D missing CoordList".to_string())
100            })?;
101            let coord_list = coord_list_attr
102                .as_list()
103                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
104            AttributeValue::parse_coordinate_list_3d(coord_list)
105        };
106
107        if positions.is_empty() {
108            return Ok(Mesh::new());
109        }
110
111        // Get faces list (attribute 2)
112        let faces_attr = entity
113            .get(2)
114            .ok_or_else(|| Error::geometry("PolygonalFaceSet missing Faces".to_string()))?;
115
116        let face_refs = faces_attr
117            .as_list()
118            .ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
119
120        // Optional point remapping list for IfcPolygonalFaceSet.
121        // CoordIndex values refer to this list when present.
122        let pn_index = entity.get(3).and_then(|attr| attr.as_list()).map(|list| {
123            list.iter()
124                .filter_map(|value| value.as_int())
125                .filter(|v| *v > 0)
126                .map(|v| v as u32)
127                .collect::<Vec<u32>>()
128        });
129
130        // Pre-allocate indices - estimate 2 triangles per face average
131        let mut indices = Vec::with_capacity(face_refs.len() * 6);
132
133        // Process each face
134        for face_ref in face_refs {
135            let face_id = face_ref
136                .as_entity_ref()
137                .ok_or_else(|| Error::geometry("Expected entity reference for face".to_string()))?;
138
139            let face_entity = decoder.decode_by_id(face_id)?;
140
141            // IfcIndexedPolygonalFace has CoordIndex at attribute 0
142            // IfcIndexedPolygonalFaceWithVoids has CoordIndex at 0 and InnerCoordIndices at 1
143            let coord_index_attr = face_entity.get(0).ok_or_else(|| {
144                Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
145            })?;
146
147            let coord_indices = coord_index_attr
148                .as_list()
149                .ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
150
151            // Parse face indices (1-based in IFC), with optional PnIndex remapping.
152            let face_indices = Self::parse_index_loop(coord_indices, pn_index.as_deref());
153            if face_indices.len() < 3 {
154                continue;
155            }
156
157            // Parse optional inner loops for IfcIndexedPolygonalFaceWithVoids.
158            let inner_indices = Self::parse_face_inner_indices(&face_entity, pn_index.as_deref());
159
160            // Triangulate the polygon face (including holes when present).
161            Self::triangulate_polygon(&face_indices, &inner_indices, &positions, &mut indices);
162        }
163
164        // Closed shells from some exporters may be consistently inward.
165        // Flip globally to outward winding when needed.
166        let is_closed = entity
167            .get(1)
168            .and_then(|a| a.as_enum())
169            .map(|v| v == "T")
170            .unwrap_or(false);
171        if is_closed {
172            Self::orient_closed_shell_outward(&positions, &mut indices);
173        }
174
175        Ok(Self::build_flat_shaded_mesh(&positions, &indices))
176    }
177
178    fn supported_types(&self) -> Vec<IfcType> {
179        vec![IfcType::IfcPolygonalFaceSet]
180    }
181}
182
183impl Default for PolygonalFaceSetProcessor {
184    fn default() -> Self {
185        Self::new()
186    }
187}