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