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