Skip to main content

ifc_lite_geometry/processors/tessellated/
triangulated.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::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
7
8use crate::router::GeometryProcessor;
9
10use super::polygonal::PolygonalFaceSetProcessor;
11
12/// TriangulatedFaceSet processor (P0)
13/// Handles IfcTriangulatedFaceSet - explicit triangle meshes
14pub struct TriangulatedFaceSetProcessor;
15
16impl TriangulatedFaceSetProcessor {
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Parse an `IfcTriangulatedFaceSet`'s positions + triangle indices and
22    /// apply the closed-shell outward orientation. Returns
23    /// `(positions, indices, flipped)` where `flipped` is whether the whole
24    /// shell was winding-flipped — the texture path needs it to keep the
25    /// parallel `TexCoordIndex` in lockstep (#961). Shared by `process` and
26    /// [`Self::process_with_texture`] so there is one parse/orient code path.
27    fn parse_positions_and_orient(
28        entity: &DecodedEntity,
29        decoder: &mut EntityDecoder,
30    ) -> Result<(Vec<f32>, Vec<u32>, bool)> {
31        // IfcTriangulatedFaceSet attributes:
32        // 0: Coordinates (IfcCartesianPointList3D)
33        // 1: Normals (optional)
34        // 2: Closed (optional)
35        // 3: CoordIndex (list of list of IfcPositiveInteger)
36
37        // Get coordinate entity reference
38        let coords_attr = entity.get(0).ok_or_else(|| {
39            Error::geometry("TriangulatedFaceSet missing Coordinates".to_string())
40        })?;
41
42        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
43            Error::geometry("Expected entity reference for Coordinates".to_string())
44        })?;
45
46        // FAST PATH: Try direct parsing of raw bytes (3-5x faster)
47        // This bypasses Token/AttributeValue allocations entirely
48        use ifc_lite_core::{extract_coordinate_list_from_entity, parse_indices_direct};
49
50        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
51            // Fast path: parse coordinates directly from raw bytes
52            // Use extract_coordinate_list_from_entity to skip entity header (#N=IFCTYPE...)
53            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
54        } else {
55            // Fallback path: use standard decoding
56            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
57
58            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
59                Error::geometry("CartesianPointList3D missing CoordList".to_string())
60            })?;
61
62            let coord_list = coord_list_attr
63                .as_list()
64                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
65
66            use ifc_lite_core::AttributeValue;
67            AttributeValue::parse_coordinate_list_3d(coord_list)
68        };
69
70        // Get face indices - try fast path first
71        let indices_attr = entity
72            .get(3)
73            .ok_or_else(|| Error::geometry("TriangulatedFaceSet missing CoordIndex".to_string()))?;
74
75        // For indices, we need to extract from the main entity's raw bytes
76        // Fast path: parse directly if we can get the raw CoordIndex section
77        let indices = if let Some(raw_entity_bytes) = decoder.get_raw_bytes(entity.id) {
78            // Find the CoordIndex attribute (4th attribute, index 3)
79            // and parse directly
80            if let Some(coord_index_bytes) = super::super::extract_coord_index_bytes(raw_entity_bytes) {
81                parse_indices_direct(coord_index_bytes)
82            } else {
83                // Fallback to standard parsing
84                let face_list = indices_attr
85                    .as_list()
86                    .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
87                use ifc_lite_core::AttributeValue;
88                AttributeValue::parse_index_list(face_list)
89            }
90        } else {
91            let face_list = indices_attr
92                .as_list()
93                .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
94            use ifc_lite_core::AttributeValue;
95            AttributeValue::parse_index_list(face_list)
96        };
97
98        // Read Closed (attribute 2): .T. means definitely closed, .F. means
99        // definitely open, $ / UNKNOWN means "not specified". Revit-exported
100        // light fixtures and similar families in IFC4 often omit Closed
101        // ($) but still author closed shells — sometimes with inward-facing
102        // winding (issue #819, IFC4TessellationComplex.ifc). Mirror the
103        // PolygonalFaceSet orientation pass but be less strict: also apply
104        // it when Closed is unknown, never when explicitly .F.
105        let closed_attr = entity.get(2);
106        let is_open = closed_attr
107            .and_then(|a| a.as_enum())
108            .map(|v| v == "F")
109            .unwrap_or(false);
110
111        let mut indices = indices;
112        let flipped = if !is_open {
113            PolygonalFaceSetProcessor::orient_closed_shell_outward(&positions, &mut indices)
114        } else {
115            false
116        };
117
118        Ok((positions, indices, flipped))
119    }
120
121    /// Tessellate a textured `IfcTriangulatedFaceSet` (#961): builds the same
122    /// flat-shaded mesh as [`process`] plus a per-vertex UV array aligned 1:1
123    /// with the emitted vertices. `map.tex_coord_index` is parallel to the
124    /// original `CoordIndex`; the same whole-shell winding flip is applied to it
125    /// so corners stay aligned after orientation.
126    pub fn process_with_texture(
127        &self,
128        entity: &DecodedEntity,
129        decoder: &mut EntityDecoder,
130        map: &crate::processors::texture::ResolvedTextureMap,
131    ) -> Result<(Mesh, Vec<f32>)> {
132        let (positions, indices, flipped) = Self::parse_positions_and_orient(entity, decoder)?;
133        let tex_coord_index = match &map.tex_coord_index {
134            Some(authored) => {
135                let mut idx = authored.clone();
136                if flipped {
137                    for tri in idx.iter_mut() {
138                        tri.swap(1, 2);
139                    }
140                }
141                idx
142            }
143            // TexCoordIndex omitted (`$`): texture vertices pair 1:1 with the
144            // face set's Coordinates, so the CoordIndex IS the UV index (#1781).
145            // Derived from the POST-orientation `indices` (0-based → 1-based),
146            // so the whole-shell winding flip is already reflected — no swap.
147            None => indices
148                .chunks_exact(3)
149                .map(|t| [t[0] + 1, t[1] + 1, t[2] + 1])
150                .collect(),
151        };
152        let (mut mesh, uvs) = PolygonalFaceSetProcessor::build_flat_shaded_mesh_with_uvs(
153            &positions,
154            &indices,
155            &map.tex_coords,
156            &tex_coord_index,
157        );
158        mesh.validate_indices();
159        Ok((mesh, uvs))
160    }
161}
162
163impl GeometryProcessor for TriangulatedFaceSetProcessor {
164    #[inline]
165    fn process(
166        &self,
167        entity: &DecodedEntity,
168        decoder: &mut EntityDecoder,
169        _schema: &IfcSchema,
170        _quality: TessellationQuality,
171    ) -> Result<Mesh> {
172        let (positions, indices, _flipped) = Self::parse_positions_and_orient(entity, decoder)?;
173
174        // Flat-shade by duplicating vertices per-triangle. Without this, the
175        // downstream per-vertex normal accumulator (`csg::calculate_normals`)
176        // averages adjacent face normals at every shared vertex, which
177        // softens crisp facet edges into a muddy gradient on faceted
178        // geometry — visible in issue #819 on `IFC4TessellationComplex.ifc`
179        // where the user contrasted ifc-lite's smoothed dome with the
180        // facet-sharp BIMVision render. `PolygonalFaceSetProcessor` already
181        // does this; bringing `IfcTriangulatedFaceSet` to parity matches
182        // IfcOpenShell / web-ifc behaviour for `Normals = $`.
183        //
184        // 3× vertex bloat. Acceptable for Revit lighting/family export
185        // sizes; if it ever becomes a bottleneck on giant tessellated
186        // models, gate this on per-edge crease angle.
187        let mut mesh = PolygonalFaceSetProcessor::build_flat_shaded_mesh(&positions, &indices);
188        mesh.validate_indices();
189        Ok(mesh)
190    }
191
192    fn supported_types(&self) -> Vec<IfcType> {
193        // IfcTriangulatedIrregularNetwork is a subtype of IfcTriangulatedFaceSet
194        // that adds an optional `ClosedOrOpen` list at the end and is used for
195        // terrain (TIN) surfaces. We don't read the extra attribute and the
196        // inherited Coordinates / Closed / CoordIndex layout is identical, so
197        // routing TIN through the same processor is correct.
198        vec![
199            IfcType::IfcTriangulatedFaceSet,
200            IfcType::IfcTriangulatedIrregularNetwork,
201        ]
202    }
203}
204
205impl Default for TriangulatedFaceSetProcessor {
206    fn default() -> Self {
207        Self::new()
208    }
209}