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}