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/// A dropped `IfcAdvancedFace` (unsupported surface, or a #4901 degree/work
14/// cap) has nowhere else to go here — neither processor has a
15/// `GeometryRouter` for `record_unsupported_item`. This trace is the honest
16/// floor. `allow`: `diag_debug!` no-ops without a tracing subscriber (wasm
17/// release), leaving both params unused there.
18#[allow(unused_variables)]
19fn trace_capped_face(face_id: u32, error: &Error) {
20    crate::diag::diag_debug!(
21        { face_id, error = %error, "skipping unsupported advanced face in surface model" }
22        else {
23            #[cfg(debug_assertions)]
24            eprintln!("[ifc-lite] Skipping unsupported advanced face #{face_id} in surface model: {error}");
25        }
26    );
27}
28
29/// Mesh every face of a surface model's face collections.
30///
31/// `IfcFaceBasedSurfaceModel.FbsmFaces` (connected face sets) and
32/// `IfcShellBasedSurfaceModel.SbsmBoundary` (open/closed shells) both hold
33/// references to entities whose attribute 0 is the face list, so one walk
34/// serves both. Two face kinds are supported:
35/// - simple faces with `IfcPolyLoop` bounds (standard BRep from most exporters)
36/// - `IfcAdvancedFace` with B-spline/planar/cylindrical surfaces (CATIA, NURBS)
37///
38/// With `rtc_file_units`, every face subtracts the offset from its f64
39/// coordinates BEFORE narrowing to f32, so national-grid surface models keep
40/// detail below one f32 ULP (#5698); `None` is the historical output.
41fn mesh_face_collections(
42    collections: &[ifc_lite_core::AttributeValue],
43    decoder: &mut EntityDecoder,
44    quality: TessellationQuality,
45    rtc_file_units: Option<(f64, f64, f64)>,
46    collection_noun: &str,
47) -> Result<Mesh> {
48    let mut all_positions = Vec::new();
49    let mut all_indices = Vec::new();
50
51    for collection_ref in collections {
52        let collection_id = collection_ref.as_entity_ref().ok_or_else(|| {
53            Error::geometry(format!("Expected entity reference for {collection_noun}"))
54        })?;
55
56        // Face IDs straight from the collection's raw bytes (attribute 0)
57        let face_ids = match decoder.get_entity_ref_list_fast(collection_id) {
58            Some(ids) => ids,
59            None => continue,
60        };
61
62        for face_id in face_ids {
63            // Decode the face entity to check its type: some exporters use
64            // IfcAdvancedFace here, which requires surface processing.
65            let face = match decoder.decode_by_id(face_id) {
66                Ok(f) => f,
67                Err(_) => continue,
68            };
69
70            if face.ifc_type == IfcType::IfcAdvancedFace {
71                // Advanced face: delegate to shared NURBS/planar/cylindrical handler.
72                // A capped B-spline curve EDGE (#4901) inside a still-OK
73                // face is deliberately NOT drained here: this processor
74                // is always reached through `GeometryRouter` (the
75                // processor registry / mapped-item dispatch), and those
76                // call sites own the single authoritative drain. Racing
77                // them for the same thread-local flag here always won
78                // (this runs first), silently swallowing the router's
79                // typed diagnostic (caught in review).
80                let (positions, indices) =
81                    match process_advanced_face(&face, decoder, quality, rtc_file_units) {
82                        Ok(result) => result,
83                        Err(ref e) => {
84                            trace_capped_face(face.id, e);
85                            continue;
86                        }
87                    };
88
89                if !positions.is_empty() {
90                    let base_idx = (all_positions.len() / 3) as u32;
91                    all_positions.extend(positions);
92                    for idx in indices {
93                        all_indices.push(base_idx + idx);
94                    }
95                }
96            } else {
97                // Simple face: extract PolyLoop points via fast path
98                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
99                    Some(ids) => ids,
100                    None => continue,
101                };
102
103                let mut outer_points: Option<Vec<Point3<f64>>> = None;
104                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
105
106                for bound_id in bound_ids {
107                    // (loop_id, orientation, is_outer) straight from raw bytes
108                    let (loop_id, orientation, is_outer) =
109                        match decoder.get_face_bound_fast(bound_id) {
110                            Some(data) => data,
111                            None => continue,
112                        };
113
114                    let mut points = match extract_loop_points_by_id(loop_id, decoder) {
115                        Some(p) => p,
116                        None => continue,
117                    };
118
119                    if !orientation {
120                        points.reverse();
121                    }
122
123                    if is_outer || outer_points.is_none() {
124                        // A second outer bound demotes the previous one to a
125                        // hole rather than dropping it (parity with
126                        // FacetedBrepProcessor); a face is otherwise lost.
127                        if outer_points.is_some() && is_outer {
128                            if let Some(prev_outer) = outer_points.take() {
129                                hole_points.push(prev_outer);
130                            }
131                        }
132                        outer_points = Some(points);
133                    } else {
134                        hole_points.push(points);
135                    }
136                }
137
138                // Triangulate the face through the shared FacetedBrep face
139                // triangulator (tri/quad fast paths, convexity test, and
140                // ear-clipping with hole support). The previous naive fan
141                // here mis-triangulated CONCAVE faces — fan triangles from
142                // vertex 0 sweep ACROSS the concavity, rendering folded
143                // sheet-metal profiles (schependomlaan "zinkwerk" covering
144                // flashings, serpentine 30-vertex end-cap loops) as
145                // stretched diagonal flaps with up to 2.4x the authored
146                // surface area — and silently dropped hole bounds.
147                if let Some(outer) = outer_points {
148                    if outer.len() >= 3 {
149                        let face_data = FaceData {
150                            outer_points: outer,
151                            hole_points,
152                        };
153                        let result = FacetedBrepProcessor::triangulate_face(
154                            &face_data,
155                            rtc_file_units.unwrap_or((0.0, 0.0, 0.0)),
156                        );
157                        let base_idx = (all_positions.len() / 3) as u32;
158                        all_positions.extend(result.positions);
159                        for idx in result.indices {
160                            all_indices.push(base_idx + idx);
161                        }
162                    }
163                }
164            }
165        }
166    }
167
168    Ok(Mesh {
169        positions: all_positions,
170        normals: Vec::new(),
171        indices: all_indices,
172        rtc_applied: rtc_file_units.is_some(),
173        welded_in_object_frame: false,
174        plane_tags: None,
175        origin: [0.0; 3],
176        instance_meta: None,
177        local_bounds: None,
178        local_to_world: None,
179    })
180}
181
182/// Mesh a surface model whose attribute 0 lists its face collections.
183/// `names` is the (missing-attribute message, collection noun) for errors.
184fn mesh_surface_model(
185    entity: &DecodedEntity,
186    decoder: &mut EntityDecoder,
187    quality: TessellationQuality,
188    rtc_file_units: Option<(f64, f64, f64)>,
189    names: (&str, &str),
190) -> Result<Mesh> {
191    let (missing_attribute, collection) = names;
192    let collections = entity
193        .get(0)
194        .ok_or_else(|| Error::geometry(missing_attribute.to_string()))?
195        .as_list()
196        .ok_or_else(|| Error::geometry(format!("Expected {collection} list")))?;
197    mesh_face_collections(collections, decoder, quality, rtc_file_units, collection)
198}
199
200// ---------- FaceBasedSurfaceModelProcessor ----------
201
202/// Handles `IfcFaceBasedSurfaceModel`: FbsmFaces (SET of IfcConnectedFaceSet)
203/// -> Face[] -> FaceBound -> PolyLoop, or AdvancedFace[] -> FaceSurface.
204pub struct FaceBasedSurfaceModelProcessor;
205
206impl FaceBasedSurfaceModelProcessor {
207    const NAMES: (&'static str, &'static str) =
208        ("FaceBasedSurfaceModel missing FbsmFaces", "face set");
209
210    pub fn new() -> Self {
211        Self
212    }
213}
214
215impl GeometryProcessor for FaceBasedSurfaceModelProcessor {
216    fn process(
217        &self,
218        entity: &DecodedEntity,
219        decoder: &mut EntityDecoder,
220        _schema: &IfcSchema,
221        quality: TessellationQuality,
222    ) -> Result<Mesh> {
223        mesh_surface_model(entity, decoder, quality, None, Self::NAMES)
224    }
225
226    fn process_in_rtc_frame(
227        &self,
228        entity: &DecodedEntity,
229        decoder: &mut EntityDecoder,
230        _schema: &IfcSchema,
231        quality: TessellationQuality,
232        rtc_file_units: (f64, f64, f64),
233    ) -> Option<Result<Mesh>> {
234        Some(mesh_surface_model(
235            entity,
236            decoder,
237            quality,
238            Some(rtc_file_units),
239            Self::NAMES,
240        ))
241    }
242
243    fn supported_types(&self) -> Vec<IfcType> {
244        vec![IfcType::IfcFaceBasedSurfaceModel]
245    }
246}
247
248impl Default for FaceBasedSurfaceModelProcessor {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254// ---------- ShellBasedSurfaceModelProcessor ----------
255
256/// Handles `IfcShellBasedSurfaceModel`: SbsmBoundary (SET of IfcOpenShell /
257/// IfcClosedShell) -> Face[] -> FaceBound -> PolyLoop, or AdvancedFace[] ->
258/// FaceSurface.
259pub struct ShellBasedSurfaceModelProcessor;
260
261impl ShellBasedSurfaceModelProcessor {
262    const NAMES: (&'static str, &'static str) =
263        ("ShellBasedSurfaceModel missing SbsmBoundary", "shell");
264
265    pub fn new() -> Self {
266        Self
267    }
268}
269
270impl GeometryProcessor for ShellBasedSurfaceModelProcessor {
271    fn process(
272        &self,
273        entity: &DecodedEntity,
274        decoder: &mut EntityDecoder,
275        _schema: &IfcSchema,
276        quality: TessellationQuality,
277    ) -> Result<Mesh> {
278        mesh_surface_model(entity, decoder, quality, None, Self::NAMES)
279    }
280
281    fn process_in_rtc_frame(
282        &self,
283        entity: &DecodedEntity,
284        decoder: &mut EntityDecoder,
285        _schema: &IfcSchema,
286        quality: TessellationQuality,
287        rtc_file_units: (f64, f64, f64),
288    ) -> Option<Result<Mesh>> {
289        Some(mesh_surface_model(
290            entity,
291            decoder,
292            quality,
293            Some(rtc_file_units),
294            Self::NAMES,
295        ))
296    }
297
298    fn supported_types(&self) -> Vec<IfcType> {
299        vec![IfcType::IfcShellBasedSurfaceModel]
300    }
301}
302
303impl Default for ShellBasedSurfaceModelProcessor {
304    fn default() -> Self {
305        Self::new()
306    }
307}