Skip to main content

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}