ifc_lite_geometry/processors/brep/surface_model.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, Point3, Result, TessellationQuality};
6use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
7
8use super::super::advanced_face::process_advanced_face;
9use super::super::helpers::{extract_loop_points_by_id, FaceData};
10use super::faceted::FacetedBrepProcessor;
11use crate::router::GeometryProcessor;
12
13// ---------- FaceBasedSurfaceModelProcessor ----------
14
15/// FaceBasedSurfaceModel processor
16/// Handles IfcFaceBasedSurfaceModel - surface model made of connected face sets
17///
18/// Supports two face types within connected face sets:
19/// - Simple faces with IfcPolyLoop bounds (standard BRep from most exporters)
20/// - IfcAdvancedFace with B-spline/planar/cylindrical surfaces (CATIA, NURBS exports)
21///
22/// Structure (simple): FaceBasedSurfaceModel -> ConnectedFaceSet[] -> Face[] -> FaceBound -> PolyLoop
23/// Structure (advanced): FaceBasedSurfaceModel -> ConnectedFaceSet[] -> AdvancedFace[] -> FaceSurface
24pub struct FaceBasedSurfaceModelProcessor;
25
26impl FaceBasedSurfaceModelProcessor {
27 pub fn new() -> Self {
28 Self
29 }
30}
31
32impl GeometryProcessor for FaceBasedSurfaceModelProcessor {
33 fn process(
34 &self,
35 entity: &DecodedEntity,
36 decoder: &mut EntityDecoder,
37 _schema: &IfcSchema,
38 quality: TessellationQuality,
39 ) -> Result<Mesh> {
40 // IfcFaceBasedSurfaceModel attributes:
41 // 0: FbsmFaces (SET of IfcConnectedFaceSet)
42
43 let faces_attr = entity.get(0).ok_or_else(|| {
44 Error::geometry("FaceBasedSurfaceModel missing FbsmFaces".to_string())
45 })?;
46
47 let face_set_refs = faces_attr
48 .as_list()
49 .ok_or_else(|| Error::geometry("Expected face set list".to_string()))?;
50
51 let mut all_positions = Vec::new();
52 let mut all_indices = Vec::new();
53
54 // Process each connected face set
55 for face_set_ref in face_set_refs {
56 let face_set_id = face_set_ref.as_entity_ref().ok_or_else(|| {
57 Error::geometry("Expected entity reference for face set".to_string())
58 })?;
59
60 // Get face IDs from ConnectedFaceSet
61 let face_ids = match decoder.get_entity_ref_list_fast(face_set_id) {
62 Some(ids) => ids,
63 None => continue,
64 };
65
66 // Process each face in the set
67 for face_id in face_ids {
68 // Decode the face entity to check its type.
69 // Some exporters use IfcAdvancedFace within ConnectedFaceSet,
70 // which requires B-spline surface processing.
71 let face = match decoder.decode_by_id(face_id) {
72 Ok(f) => f,
73 Err(_) => continue,
74 };
75
76 if face.ifc_type == IfcType::IfcAdvancedFace {
77 // Advanced face: delegate to shared NURBS/planar/cylindrical handler
78 let (positions, indices) = match process_advanced_face(&face, decoder, quality) {
79 Ok(result) => result,
80 Err(_) => continue,
81 };
82
83 if !positions.is_empty() {
84 let base_idx = (all_positions.len() / 3) as u32;
85 all_positions.extend(positions);
86 for idx in indices {
87 all_indices.push(base_idx + idx);
88 }
89 }
90 } else {
91 // Simple face: extract PolyLoop points via fast path
92 let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
93 Some(ids) => ids,
94 None => continue,
95 };
96
97 let mut outer_points: Option<Vec<Point3<f64>>> = None;
98 let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
99
100 for bound_id in bound_ids {
101 // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
102 // get_face_bound_fast returns (loop_id, orientation, is_outer)
103 let (loop_id, orientation, is_outer) =
104 match decoder.get_face_bound_fast(bound_id) {
105 Some(data) => data,
106 None => continue,
107 };
108
109 // Get loop points using shared helper
110 let mut points = match extract_loop_points_by_id(loop_id, decoder) {
111 Some(p) => p,
112 None => continue,
113 };
114
115 if !orientation {
116 points.reverse();
117 }
118
119 if is_outer || outer_points.is_none() {
120 // A second outer bound demotes the previous one to a
121 // hole rather than dropping it (parity with
122 // FacetedBrepProcessor); a face is otherwise lost.
123 if outer_points.is_some() && is_outer {
124 if let Some(prev_outer) = outer_points.take() {
125 hole_points.push(prev_outer);
126 }
127 }
128 outer_points = Some(points);
129 } else {
130 hole_points.push(points);
131 }
132 }
133
134 // Triangulate the face through the shared FacetedBrep face
135 // triangulator (tri/quad fast paths, convexity test, and
136 // ear-clipping with hole support). The previous naive fan
137 // here mis-triangulated CONCAVE faces — fan triangles from
138 // vertex 0 sweep ACROSS the concavity, rendering folded
139 // sheet-metal profiles (schependomlaan "zinkwerk" covering
140 // flashings, serpentine 30-vertex end-cap loops) as
141 // stretched diagonal flaps with up to 2.4x the authored
142 // surface area — and silently dropped hole bounds.
143 if let Some(outer) = outer_points {
144 if outer.len() >= 3 {
145 let face_data = FaceData {
146 outer_points: outer,
147 hole_points,
148 };
149 let result = FacetedBrepProcessor::triangulate_face(
150 &face_data,
151 (0.0, 0.0, 0.0),
152 );
153 let base_idx = (all_positions.len() / 3) as u32;
154 all_positions.extend(result.positions);
155 for idx in result.indices {
156 all_indices.push(base_idx + idx);
157 }
158 }
159 }
160 }
161 }
162 }
163
164 Ok(Mesh {
165 positions: all_positions,
166 normals: Vec::new(),
167 indices: all_indices,
168 rtc_applied: false,
169 origin: [0.0; 3], instance_meta: None, local_bounds: None, local_to_world: None })
170 }
171
172 fn supported_types(&self) -> Vec<IfcType> {
173 vec![IfcType::IfcFaceBasedSurfaceModel]
174 }
175}
176
177impl Default for FaceBasedSurfaceModelProcessor {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183// ---------- ShellBasedSurfaceModelProcessor ----------
184
185/// ShellBasedSurfaceModel processor
186/// Handles IfcShellBasedSurfaceModel - surface model made of shells
187///
188/// Supports two face types within shells:
189/// - Simple faces with IfcPolyLoop bounds (standard BRep from most exporters)
190/// - IfcAdvancedFace with B-spline/planar/cylindrical surfaces (CATIA, NURBS exports)
191///
192/// Structure (simple): ShellBasedSurfaceModel -> Shell[] -> Face[] -> FaceBound -> PolyLoop
193/// Structure (advanced): ShellBasedSurfaceModel -> Shell[] -> AdvancedFace[] -> FaceSurface
194pub struct ShellBasedSurfaceModelProcessor;
195
196impl ShellBasedSurfaceModelProcessor {
197 pub fn new() -> Self {
198 Self
199 }
200}
201
202impl GeometryProcessor for ShellBasedSurfaceModelProcessor {
203 fn process(
204 &self,
205 entity: &DecodedEntity,
206 decoder: &mut EntityDecoder,
207 _schema: &IfcSchema,
208 quality: TessellationQuality,
209 ) -> Result<Mesh> {
210 // IfcShellBasedSurfaceModel attributes:
211 // 0: SbsmBoundary (SET of IfcShell - either IfcOpenShell or IfcClosedShell)
212
213 let shells_attr = entity.get(0).ok_or_else(|| {
214 Error::geometry("ShellBasedSurfaceModel missing SbsmBoundary".to_string())
215 })?;
216
217 let shell_refs = shells_attr
218 .as_list()
219 .ok_or_else(|| Error::geometry("Expected shell list".to_string()))?;
220
221 let mut all_positions = Vec::new();
222 let mut all_indices = Vec::new();
223
224 // Process each shell
225 for shell_ref in shell_refs {
226 let shell_id = shell_ref.as_entity_ref().ok_or_else(|| {
227 Error::geometry("Expected entity reference for shell".to_string())
228 })?;
229
230 // Get face IDs from Shell (IfcOpenShell or IfcClosedShell)
231 // Both have CfsFaces as attribute 0
232 let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
233 Some(ids) => ids,
234 None => continue,
235 };
236
237 // Process each face in the shell
238 for face_id in face_ids {
239 // Decode the face entity to check its type.
240 // CATIA and other NURBS-based exporters use IfcAdvancedFace within
241 // IfcOpenShell/IfcClosedShell, which requires B-spline surface processing
242 // instead of simple PolyLoop extraction.
243 let face = match decoder.decode_by_id(face_id) {
244 Ok(f) => f,
245 Err(_) => continue,
246 };
247
248 if face.ifc_type == IfcType::IfcAdvancedFace {
249 // Advanced face: delegate to shared NURBS/planar/cylindrical handler
250 let (positions, indices) = match process_advanced_face(&face, decoder, quality) {
251 Ok(result) => result,
252 Err(_) => continue,
253 };
254
255 if !positions.is_empty() {
256 let base_idx = (all_positions.len() / 3) as u32;
257 all_positions.extend(positions);
258 for idx in indices {
259 all_indices.push(base_idx + idx);
260 }
261 }
262 } else {
263 // Simple face: extract PolyLoop points via fast path
264 let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
265 Some(ids) => ids,
266 None => continue,
267 };
268
269 let mut outer_points: Option<Vec<Point3<f64>>> = None;
270 let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
271
272 for bound_id in bound_ids {
273 // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
274 let (loop_id, orientation, is_outer) =
275 match decoder.get_face_bound_fast(bound_id) {
276 Some(data) => data,
277 None => continue,
278 };
279
280 // Get loop points using shared helper
281 let mut points = match extract_loop_points_by_id(loop_id, decoder) {
282 Some(p) => p,
283 None => continue,
284 };
285
286 if !orientation {
287 points.reverse();
288 }
289
290 if is_outer || outer_points.is_none() {
291 // A second outer bound demotes the previous one to a
292 // hole rather than dropping it (parity with
293 // FacetedBrepProcessor); a face is otherwise lost.
294 if outer_points.is_some() && is_outer {
295 if let Some(prev_outer) = outer_points.take() {
296 hole_points.push(prev_outer);
297 }
298 }
299 outer_points = Some(points);
300 } else {
301 hole_points.push(points);
302 }
303 }
304
305 // Triangulate the face through the shared FacetedBrep face
306 // triangulator (tri/quad fast paths, convexity test, and
307 // ear-clipping with hole support). The previous naive fan
308 // here mis-triangulated CONCAVE faces — fan triangles from
309 // vertex 0 sweep ACROSS the concavity, rendering folded
310 // sheet-metal profiles (schependomlaan "zinkwerk" covering
311 // flashings, serpentine 30-vertex end-cap loops) as
312 // stretched diagonal flaps with up to 2.4x the authored
313 // surface area — and silently dropped hole bounds.
314 if let Some(outer) = outer_points {
315 if outer.len() >= 3 {
316 let face_data = FaceData {
317 outer_points: outer,
318 hole_points,
319 };
320 let result = FacetedBrepProcessor::triangulate_face(
321 &face_data,
322 (0.0, 0.0, 0.0),
323 );
324 let base_idx = (all_positions.len() / 3) as u32;
325 all_positions.extend(result.positions);
326 for idx in result.indices {
327 all_indices.push(base_idx + idx);
328 }
329 }
330 }
331 }
332 }
333 }
334
335 Ok(Mesh {
336 positions: all_positions,
337 normals: Vec::new(),
338 indices: all_indices,
339 rtc_applied: false,
340 origin: [0.0; 3], instance_meta: None, local_bounds: None, local_to_world: None })
341 }
342
343 fn supported_types(&self) -> Vec<IfcType> {
344 vec![IfcType::IfcShellBasedSurfaceModel]
345 }
346}
347
348impl Default for ShellBasedSurfaceModelProcessor {
349 fn default() -> Self {
350 Self::new()
351 }
352}