ifc_lite_geometry/processors/brep/
faceted.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::helpers::{FaceData, FaceResult};
9use crate::router::GeometryProcessor;
10
11/// Minimum face count at which parallel (rayon) triangulation pays off. Below
12/// this, the fork-join dispatch overhead exceeds the work — real-world BReps are
13/// overwhelmingly tiny (6–50 faces of trivial tri/quad/convex fast-path geometry),
14/// so dispatching each tiny shell through rayon costs far more than it saves
15/// (worse under the per-element worker pool, where this is nested parallelism).
16/// The serial and parallel paths produce byte-identical output: `collect`
17/// preserves index order and each face's f32 result is computed identically.
18const PAR_FACE_THRESHOLD: usize = 64;
19
20// ---------- FacetedBrepProcessor ----------
21
22/// FacetedBrep processor
23/// Handles IfcFacetedBrep - explicit mesh with faces
24/// Supports faces with inner bounds (holes)
25/// Uses parallel triangulation for large BREPs
26pub struct FacetedBrepProcessor;
27
28impl FacetedBrepProcessor {
29    pub fn new() -> Self {
30        Self
31    }
32
33    /// Extract polygon points from a loop entity
34    /// Uses fast path for CartesianPoint extraction to avoid decode overhead
35    #[allow(dead_code)]
36    #[inline]
37    fn extract_loop_points(
38        &self,
39        loop_entity: &DecodedEntity,
40        decoder: &mut EntityDecoder,
41    ) -> Option<Vec<Point3<f64>>> {
42        // Try to get Polygon attribute (attribute 0) - IfcPolyLoop has this
43        let polygon_attr = loop_entity.get(0)?;
44
45        // Get the list of point references directly
46        let point_refs = polygon_attr.as_list()?;
47
48        // Pre-allocate with known size
49        let mut polygon_points = Vec::with_capacity(point_refs.len());
50
51        for point_ref in point_refs {
52            let point_id = point_ref.as_entity_ref()?;
53
54            // Try fast path first
55            if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
56                polygon_points.push(Point3::new(x, y, z));
57            } else {
58                // Fallback to standard path if fast extraction fails
59                let point = decoder.decode_by_id(point_id).ok()?;
60                let coords_attr = point.get(0)?;
61                let coords = coords_attr.as_list()?;
62                use ifc_lite_core::AttributeValue;
63                let x = coords.first().and_then(|v: &AttributeValue| v.as_float())?;
64                let y = coords.get(1).and_then(|v: &AttributeValue| v.as_float())?;
65                let z = coords.get(2).and_then(|v: &AttributeValue| v.as_float())?;
66                polygon_points.push(Point3::new(x, y, z));
67            }
68        }
69
70        if polygon_points.len() >= 3 {
71            Some(polygon_points)
72        } else {
73            None
74        }
75    }
76
77    /// Extract polygon points using ultra-fast path from loop entity ID
78    /// Uses cached coordinate extraction - points are cached across faces
79    /// This is the fastest path for files with shared cartesian points
80    #[inline]
81    fn extract_loop_points_fast(
82        &self,
83        loop_entity_id: u32,
84        decoder: &mut EntityDecoder,
85    ) -> Option<Vec<Point3<f64>>> {
86        // ULTRA-FAST PATH with CACHING: Get coordinates with point cache
87        // Many faces share the same cartesian points, so caching avoids
88        // re-parsing the same point data multiple times
89        let coords = decoder.get_polyloop_coords_cached(loop_entity_id)?;
90
91        // Convert to Point3 - pre-allocated in get_polyloop_coords_cached
92        let polygon_points: Vec<Point3<f64>> = coords
93            .into_iter()
94            .map(|(x, y, z)| Point3::new(x, y, z))
95            .collect();
96
97        if polygon_points.len() >= 3 {
98            Some(polygon_points)
99        } else {
100            None
101        }
102    }
103
104    /// Triangulate a single face (can be called in parallel)
105    /// Optimized with fast paths for simple faces.
106    ///
107    /// `rtc`: RTC offset subtracted from f64 coordinates BEFORE f32 conversion.
108    /// For infrastructure models with world-space coordinates (e.g. Y ≈ 6.2M),
109    /// f32 only has ~0.5m precision. Subtracting RTC in f64 first brings
110    /// values near origin where f32 has sub-mm precision, eliminating jitter.
111    #[inline]
112    pub(super) fn triangulate_face(face: &FaceData, rtc: (f64, f64, f64)) -> FaceResult {
113        let n = face.outer_points.len();
114
115        // FAST PATH: Triangle without holes - no triangulation needed
116        if n == 3 && face.hole_points.is_empty() {
117            let mut positions = Vec::with_capacity(9);
118            for point in &face.outer_points {
119                positions.push((point.x - rtc.0) as f32);
120                positions.push((point.y - rtc.1) as f32);
121                positions.push((point.z - rtc.2) as f32);
122            }
123            return FaceResult {
124                positions,
125                indices: vec![0, 1, 2],
126            };
127        }
128
129        // FAST PATH: Quad without holes - simple fan
130        if n == 4 && face.hole_points.is_empty() {
131            let mut positions = Vec::with_capacity(12);
132            for point in &face.outer_points {
133                positions.push((point.x - rtc.0) as f32);
134                positions.push((point.y - rtc.1) as f32);
135                positions.push((point.z - rtc.2) as f32);
136            }
137            return FaceResult {
138                positions,
139                indices: vec![0, 1, 2, 0, 2, 3],
140            };
141        }
142
143        // FAST PATH: Simple convex polygon without holes
144        if face.hole_points.is_empty() && n <= 8 {
145            // Check if convex by testing cross products in 3D
146            let mut is_convex = true;
147            if n > 4 {
148                use crate::triangulation::calculate_polygon_normal;
149                let normal = calculate_polygon_normal(&face.outer_points);
150                let mut sign = 0i8;
151
152                for i in 0..n {
153                    let p0 = &face.outer_points[i];
154                    let p1 = &face.outer_points[(i + 1) % n];
155                    let p2 = &face.outer_points[(i + 2) % n];
156
157                    let v1 = p1 - p0;
158                    let v2 = p2 - p1;
159                    let cross = v1.cross(&v2);
160                    let dot = cross.dot(&normal);
161
162                    if dot.abs() > 1e-10 {
163                        let current_sign = if dot > 0.0 { 1i8 } else { -1i8 };
164                        if sign == 0 {
165                            sign = current_sign;
166                        } else if sign != current_sign {
167                            is_convex = false;
168                            break;
169                        }
170                    }
171                }
172            }
173
174            if is_convex {
175                let mut positions = Vec::with_capacity(n * 3);
176                for point in &face.outer_points {
177                    positions.push((point.x - rtc.0) as f32);
178                    positions.push((point.y - rtc.1) as f32);
179                    positions.push((point.z - rtc.2) as f32);
180                }
181                let mut indices = Vec::with_capacity((n - 2) * 3);
182                for i in 1..n - 1 {
183                    indices.push(0);
184                    indices.push(i as u32);
185                    indices.push(i as u32 + 1);
186                }
187                return FaceResult { positions, indices };
188            }
189        }
190
191        // SLOW PATH: Complex polygon or polygon with holes
192        use crate::triangulation::{
193            calculate_polygon_normal, project_to_2d, project_to_2d_with_basis,
194            triangulate_polygon_with_holes,
195        };
196
197        let mut positions = Vec::new();
198        let mut indices = Vec::new();
199
200        // Calculate face normal from outer boundary
201        let normal = calculate_polygon_normal(&face.outer_points);
202
203        // Project outer boundary to 2D and get the coordinate system
204        let (outer_2d, u_axis, v_axis, origin) = project_to_2d(&face.outer_points, &normal);
205
206        // Project holes to 2D using the SAME coordinate system as the outer boundary
207        let holes_2d: Vec<Vec<nalgebra::Point2<f64>>> = face
208            .hole_points
209            .iter()
210            .map(|hole| project_to_2d_with_basis(hole, &u_axis, &v_axis, &origin))
211            .collect();
212
213        // Triangulate with holes
214        let tri_indices = match triangulate_polygon_with_holes(&outer_2d, &holes_2d) {
215            Ok(idx) => idx,
216            Err(_) => {
217                // Fallback to simple fan triangulation without holes
218                for point in &face.outer_points {
219                    positions.push((point.x - rtc.0) as f32);
220                    positions.push((point.y - rtc.1) as f32);
221                    positions.push((point.z - rtc.2) as f32);
222                }
223                for i in 1..face.outer_points.len() - 1 {
224                    indices.push(0);
225                    indices.push(i as u32);
226                    indices.push(i as u32 + 1);
227                }
228                return FaceResult { positions, indices };
229            }
230        };
231
232        // Combine all 3D points (outer + holes) in the same order as 2D
233        let mut all_points_3d: Vec<&Point3<f64>> = face.outer_points.iter().collect();
234        for hole in &face.hole_points {
235            all_points_3d.extend(hole.iter());
236        }
237
238        // Add vertices (with RTC subtraction in f64 for full precision)
239        for point in &all_points_3d {
240            positions.push((point.x - rtc.0) as f32);
241            positions.push((point.y - rtc.1) as f32);
242            positions.push((point.z - rtc.2) as f32);
243        }
244
245        // Add triangle indices
246        for i in (0..tri_indices.len()).step_by(3) {
247            if i + 2 >= tri_indices.len() {
248                break;
249            }
250            indices.push(tri_indices[i] as u32);
251            indices.push(tri_indices[i + 1] as u32);
252            indices.push(tri_indices[i + 2] as u32);
253        }
254
255        FaceResult { positions, indices }
256    }
257}
258
259impl FacetedBrepProcessor {
260    /// Mesh the Brep. With `rtc_file_units`, the offset is subtracted from the
261    /// f64 loop coordinates during f32 conversion, preserving sub-centimetre
262    /// detail at national-grid magnitudes (e.g. Y ≈ 6.2M metres).
263    fn process_rebased(
264        &self,
265        entity: &DecodedEntity,
266        decoder: &mut EntityDecoder,
267        rtc_file_units: Option<(f64, f64, f64)>,
268    ) -> Result<Mesh> {
269        use rayon::prelude::*;
270
271        // IfcFacetedBrep attributes:
272        // 0: Outer (IfcClosedShell)
273        let shell_attr = entity
274            .get(0)
275            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
276        let shell_id = shell_attr
277            .as_entity_ref()
278            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
279
280        // FAST PATH: Get face IDs directly from ClosedShell raw bytes
281        let face_ids = decoder
282            .get_entity_ref_list_fast(shell_id)
283            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
284
285        // PHASE 1: Sequential - Extract all face data from IFC entities
286        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
287        for face_id in face_ids {
288            // FAST PATH: Get bound IDs directly from Face raw bytes
289            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
290                Some(ids) => ids,
291                None => continue,
292            };
293
294            // Separate outer bound from inner bounds (holes)
295            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
296            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
297            for bound_id in bound_ids {
298                // (loop_id, orientation, is_outer) straight from raw bytes
299                let (loop_id, orientation, is_outer) = match decoder.get_face_bound_fast(bound_id) {
300                    Some(data) => data,
301                    None => continue,
302                };
303                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
304                    Some(p) => p,
305                    None => continue,
306                };
307                if !orientation {
308                    points.reverse();
309                }
310                if is_outer || outer_bound_points.is_none() {
311                    if outer_bound_points.is_some() && is_outer {
312                        if let Some(prev_outer) = outer_bound_points.take() {
313                            hole_points.push(prev_outer);
314                        }
315                    }
316                    outer_bound_points = Some(points);
317                } else {
318                    hole_points.push(points);
319                }
320            }
321            if let Some(outer_points) = outer_bound_points {
322                face_data_list.push(FaceData {
323                    outer_points,
324                    hole_points,
325                });
326            }
327        }
328
329        // PHASE 2: Triangulate all faces (serial below PAR_FACE_THRESHOLD to
330        // avoid rayon fork-join overhead; both orders are byte-identical).
331        let rtc = rtc_file_units.unwrap_or((0.0, 0.0, 0.0));
332        let face_results: Vec<FaceResult> = if face_data_list.len() < PAR_FACE_THRESHOLD {
333            face_data_list
334                .iter()
335                .map(|face| Self::triangulate_face(face, rtc))
336                .collect()
337        } else {
338            face_data_list
339                .par_iter()
340                .map(|face| Self::triangulate_face(face, rtc))
341                .collect()
342        };
343
344        // PHASE 3: Sequential - Merge all face results into final mesh
345        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
346        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
347        let mut positions = Vec::with_capacity(total_positions);
348        let mut indices = Vec::with_capacity(total_indices);
349        for result in face_results {
350            let base_idx = (positions.len() / 3) as u32;
351            positions.extend(result.positions);
352            for idx in result.indices {
353                indices.push(base_idx + idx);
354            }
355        }
356
357        Ok(Mesh {
358            positions,
359            normals: Vec::new(),
360            indices,
361            rtc_applied: rtc_file_units.is_some(),
362            origin: [0.0; 3],
363            instance_meta: None,
364            local_bounds: None,
365            local_to_world: None,
366            welded_in_object_frame: false,
367            plane_tags: None,
368        })
369    }
370}
371
372impl FacetedBrepProcessor {
373    /// Mesh the Brep rebased by `rtc` (file units) before f32 narrowing; the
374    /// same work as [`GeometryProcessor::process_in_rtc_frame`].
375    pub fn process_with_rtc(
376        &self,
377        entity: &DecodedEntity,
378        decoder: &mut EntityDecoder,
379        _schema: &IfcSchema,
380        rtc: (f64, f64, f64),
381    ) -> Result<Mesh> {
382        self.process_rebased(entity, decoder, Some(rtc))
383    }
384}
385
386impl GeometryProcessor for FacetedBrepProcessor {
387    fn process(
388        &self,
389        entity: &DecodedEntity,
390        decoder: &mut EntityDecoder,
391        _schema: &IfcSchema,
392        _quality: TessellationQuality,
393    ) -> Result<Mesh> {
394        self.process_rebased(entity, decoder, None)
395    }
396
397    fn process_in_rtc_frame(
398        &self,
399        entity: &DecodedEntity,
400        decoder: &mut EntityDecoder,
401        schema: &IfcSchema,
402        _quality: TessellationQuality,
403        rtc_file_units: (f64, f64, f64),
404    ) -> Option<Result<Mesh>> {
405        Some(self.process_with_rtc(entity, decoder, schema, rtc_file_units))
406    }
407
408    fn supported_types(&self) -> Vec<IfcType> {
409        vec![IfcType::IfcFacetedBrep]
410    }
411}
412
413impl Default for FacetedBrepProcessor {
414    fn default() -> Self {
415        Self::new()
416    }
417}