ifc_lite_geometry/processors/
advanced.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
5//! AdvancedBrep processor - NURBS/B-spline surfaces.
6//!
7//! Handles IfcAdvancedBrep and IfcAdvancedBrepWithVoids.
8//! Delegates per-face processing to shared advanced_face module.
9
10use crate::{Error, Mesh, Result, TessellationQuality};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
12
13use crate::router::GeometryProcessor;
14use super::advanced_face::{
15    parse_rational_weights, process_advanced_face, process_bspline_face,
16};
17
18/// A face that fails to triangulate (e.g. an un-triangulable holed planar
19/// face, #5053) is skipped rather than aborting the whole solid, mirroring
20/// `FaceBasedSurfaceModelProcessor::process` in `brep/surface_model.rs`.
21/// `allow`: `diag_debug!` no-ops without a tracing subscriber (wasm
22/// release), leaving both params unused there. No legacy `eprintln!`
23/// fallback: this is a recovery path, not an anomaly, so it must stay
24/// silent on stderr in normal (non-`observability`) builds — see the
25/// `diag_debug!` docs on passing an empty `else {}` to compile out
26/// entirely.
27#[allow(unused_variables)]
28fn trace_capped_advanced_brep_face(face_id: u32, error: &Error) {
29    crate::diag::diag_debug!(
30        { face_id, error = %error, "skipping unsupported advanced face in advanced_brep" }
31        else {}
32    );
33}
34
35/// AdvancedBrep processor
36/// Handles IfcAdvancedBrep and IfcAdvancedBrepWithVoids - NURBS/B-spline surfaces
37/// Supports planar faces and B-spline surface tessellation
38pub struct AdvancedBrepProcessor;
39
40impl AdvancedBrepProcessor {
41    pub fn new() -> Self {
42        Self
43    }
44}
45
46impl AdvancedBrepProcessor {
47    /// Mesh the Brep; `rtc_file_units` is removed from every face in f64
48    /// before f32 narrowing (#5698).
49    fn process_rebased(
50        entity: &DecodedEntity,
51        decoder: &mut EntityDecoder,
52        quality: TessellationQuality,
53        rtc_file_units: Option<(f64, f64, f64)>,
54    ) -> Result<Mesh> {
55        // IfcAdvancedBrep attributes:
56        // 0: Outer (IfcClosedShell)
57
58        // Get the outer shell
59        let shell_attr = entity
60            .get(0)
61            .ok_or_else(|| Error::geometry("AdvancedBrep missing Outer shell".to_string()))?;
62
63        let shell = decoder
64            .resolve_ref(shell_attr)?
65            .ok_or_else(|| Error::geometry("Failed to resolve Outer shell".to_string()))?;
66
67        // Get faces from the shell (IfcClosedShell.CfsFaces)
68        let faces_attr = shell
69            .get(0)
70            .ok_or_else(|| Error::geometry("ClosedShell missing CfsFaces".to_string()))?;
71
72        let faces = faces_attr
73            .as_list()
74            .ok_or_else(|| Error::geometry("Expected face list".to_string()))?;
75
76        let mut all_positions = Vec::new();
77        let mut all_indices = Vec::new();
78
79        #[cfg(any(feature = "debug_geometry", feature = "observability"))]
80        let mut empty_faces: Vec<(u32, String)> = Vec::new();
81
82        for face_ref in faces {
83            if let Some(face_id) = face_ref.as_entity_ref() {
84                let face = decoder.decode_by_id(face_id)?;
85
86                // Delegate to shared advanced face processing. A face that
87                // fails (rather than one that meshes to nothing) is caught
88                // and skipped here too, so one un-triangulable holed face
89                // degrades to a per-face loss instead of aborting the whole
90                // solid (#5053) — and is still recorded in `empty_faces`
91                // below so the loss isn't silent.
92                let face_mesh = process_advanced_face(&face, decoder, quality, rtc_file_units);
93                let (positions, indices) = match face_mesh {
94                    Ok(result) => result,
95                    Err(ref e) => {
96                        trace_capped_advanced_brep_face(face_id, e);
97                        #[cfg(any(feature = "debug_geometry", feature = "observability"))]
98                        {
99                            let surface_kind = face
100                                .get(1)
101                                .and_then(|a| decoder.resolve_ref(a).ok().flatten())
102                                .map(|s| s.ifc_type.as_str().to_string())
103                                .unwrap_or_else(|| "<unknown>".to_string());
104                            empty_faces.push((face_id, surface_kind));
105                        }
106                        continue;
107                    }
108                };
109
110                if !positions.is_empty() {
111                    // Merge into combined mesh
112                    let base_idx = (all_positions.len() / 3) as u32;
113                    all_positions.extend(positions);
114                    for idx in indices {
115                        all_indices.push(base_idx + idx);
116                    }
117                } else {
118                    #[cfg(any(feature = "debug_geometry", feature = "observability"))]
119                    {
120                        let surface_kind = face
121                            .get(1)
122                            .and_then(|a| decoder.resolve_ref(a).ok().flatten())
123                            .map(|s| s.ifc_type.as_str().to_string())
124                            .unwrap_or_else(|| "<unknown>".to_string());
125                        empty_faces.push((face_id, surface_kind));
126                    }
127                }
128            }
129        }
130
131        // Geometry loss on an advanced brep (faces that meshed to nothing) is
132        // a genuine anomaly: warn-level under observability. The legacy
133        // stderr line stays gated behind debug_geometry as before.
134        #[cfg(any(feature = "debug_geometry", feature = "observability"))]
135        if !empty_faces.is_empty() {
136            crate::diag::diag_warn!(
137                { entity_id = entity.id, empty_faces = empty_faces.len(),
138                  faces = ?empty_faces, "advanced_brep: entity produced empty faces" }
139                else {
140                    #[cfg(feature = "debug_geometry")]
141                    eprintln!(
142                        "[ifc-lite][advanced_brep] entity #{} produced {} empty face(s): {:?}",
143                        entity.id,
144                        empty_faces.len(),
145                        empty_faces
146                    );
147                }
148            );
149        }
150
151        Ok(Mesh {
152            positions: all_positions,
153            normals: Vec::new(),
154            indices: all_indices,
155            rtc_applied: rtc_file_units.is_some(),
156            welded_in_object_frame: false,
157            plane_tags: None,
158            origin: [0.0; 3],        instance_meta: None, local_bounds: None, local_to_world: None })
159    }
160}
161
162impl GeometryProcessor for AdvancedBrepProcessor {
163    fn process(
164        &self,
165        entity: &DecodedEntity,
166        decoder: &mut EntityDecoder,
167        _schema: &IfcSchema,
168        quality: TessellationQuality,
169    ) -> Result<Mesh> {
170        Self::process_rebased(entity, decoder, quality, None)
171    }
172
173    fn process_in_rtc_frame(
174        &self,
175        entity: &DecodedEntity,
176        decoder: &mut EntityDecoder,
177        _schema: &IfcSchema,
178        quality: TessellationQuality,
179        rtc_file_units: (f64, f64, f64),
180    ) -> Option<Result<Mesh>> {
181        Some(Self::process_rebased(entity, decoder, quality, Some(rtc_file_units)))
182    }
183
184    fn supported_types(&self) -> Vec<IfcType> {
185        vec![IfcType::IfcAdvancedBrep, IfcType::IfcAdvancedBrepWithVoids]
186    }
187}
188
189impl Default for AdvancedBrepProcessor {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195/// Standalone B-spline surface processor.
196///
197/// Handles `IfcBSplineSurfaceWithKnots` and `IfcRationalBSplineSurfaceWithKnots`
198/// when they appear directly as items inside an `IfcShapeRepresentation` (e.g.
199/// a `Surface3D` rep), without being wrapped in an `IfcAdvancedFace`.
200pub struct BSplineSurfaceProcessor;
201
202impl BSplineSurfaceProcessor {
203    pub fn new() -> Self {
204        Self
205    }
206
207    /// Mesh the surface; `rtc_file_units` shifts the control net in f64
208    /// before f32 narrowing (#5698).
209    fn process_rebased(
210        entity: &DecodedEntity,
211        decoder: &mut EntityDecoder,
212        quality: TessellationQuality,
213        rtc_file_units: Option<(f64, f64, f64)>,
214    ) -> Result<Mesh> {
215        let weights = if entity.ifc_type == IfcType::IfcRationalBSplineSurfaceWithKnots {
216            parse_rational_weights(entity)
217        } else {
218            None
219        };
220        let (positions, indices) = process_bspline_face(
221            entity,
222            decoder,
223            weights.as_deref(),
224            quality,
225            rtc_file_units.unwrap_or((0.0, 0.0, 0.0)),
226        )?;
227        Ok(Mesh {
228            positions,
229            normals: Vec::new(),
230            indices,
231            rtc_applied: rtc_file_units.is_some(),
232            welded_in_object_frame: false,
233            plane_tags: None,
234            origin: [0.0; 3],
235            instance_meta: None,
236            local_bounds: None,
237            local_to_world: None,
238        })
239    }
240}
241
242impl Default for BSplineSurfaceProcessor {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248impl GeometryProcessor for BSplineSurfaceProcessor {
249    fn process(
250        &self,
251        entity: &DecodedEntity,
252        decoder: &mut EntityDecoder,
253        _schema: &IfcSchema,
254        quality: TessellationQuality,
255    ) -> Result<Mesh> {
256        Self::process_rebased(entity, decoder, quality, None)
257    }
258
259    fn process_in_rtc_frame(
260        &self,
261        entity: &DecodedEntity,
262        decoder: &mut EntityDecoder,
263        _schema: &IfcSchema,
264        quality: TessellationQuality,
265        rtc_file_units: (f64, f64, f64),
266    ) -> Option<Result<Mesh>> {
267        Some(Self::process_rebased(entity, decoder, quality, Some(rtc_file_units)))
268    }
269
270    fn supported_types(&self) -> Vec<IfcType> {
271        vec![
272            IfcType::IfcBSplineSurfaceWithKnots,
273            IfcType::IfcRationalBSplineSurfaceWithKnots,
274        ]
275    }
276}