Skip to main content

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}
259
260impl FacetedBrepProcessor {
261    /// Process FacetedBrep with RTC offset applied during f64→f32 conversion.
262    /// This preserves sub-centimeter precision for infrastructure models where
263    /// vertices have large world coordinates (e.g. Y ≈ 6.2M meters).
264    pub fn process_with_rtc(
265        &self,
266        entity: &DecodedEntity,
267        decoder: &mut EntityDecoder,
268        _schema: &IfcSchema,
269        rtc: (f64, f64, f64),
270    ) -> Result<Mesh> {
271        use rayon::prelude::*;
272
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        let face_ids = decoder
280            .get_entity_ref_list_fast(shell_id)
281            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
282
283        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
284        for face_id in face_ids {
285            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
286                Some(ids) => ids,
287                None => continue,
288            };
289            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
290            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
291            for bound_id in bound_ids {
292                let (loop_id, orientation, is_outer) = match decoder.get_face_bound_fast(bound_id) {
293                    Some(data) => data,
294                    None => continue,
295                };
296                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
297                    Some(p) => p,
298                    None => continue,
299                };
300                if !orientation {
301                    points.reverse();
302                }
303                if is_outer || outer_bound_points.is_none() {
304                    if outer_bound_points.is_some() && is_outer {
305                        if let Some(prev_outer) = outer_bound_points.take() {
306                            hole_points.push(prev_outer);
307                        }
308                    }
309                    outer_bound_points = Some(points);
310                } else {
311                    hole_points.push(points);
312                }
313            }
314            if let Some(outer_points) = outer_bound_points {
315                face_data_list.push(FaceData {
316                    outer_points,
317                    hole_points,
318                });
319            }
320        }
321
322        // Serial for small shells (avoids rayon fork-join overhead); byte-identical.
323        let face_results: Vec<FaceResult> = if face_data_list.len() < PAR_FACE_THRESHOLD {
324            face_data_list
325                .iter()
326                .map(|face| Self::triangulate_face(face, rtc))
327                .collect()
328        } else {
329            face_data_list
330                .par_iter()
331                .map(|face| Self::triangulate_face(face, rtc))
332                .collect()
333        };
334
335        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
336        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
337        let mut positions = Vec::with_capacity(total_positions);
338        let mut indices = Vec::with_capacity(total_indices);
339        for result in face_results {
340            let base_idx = (positions.len() / 3) as u32;
341            positions.extend(result.positions);
342            for idx in result.indices {
343                indices.push(base_idx + idx);
344            }
345        }
346        Ok(Mesh {
347            positions,
348            normals: Vec::new(),
349            indices,
350            rtc_applied: true, // RTC already subtracted during f64→f32 conversion
351            origin: [0.0; 3],
352        instance_meta: None, local_bounds: None, local_to_world: None })
353    }
354}
355
356impl GeometryProcessor for FacetedBrepProcessor {
357    fn process(
358        &self,
359        entity: &DecodedEntity,
360        decoder: &mut EntityDecoder,
361        _schema: &IfcSchema,
362        _quality: TessellationQuality,
363    ) -> Result<Mesh> {
364        use rayon::prelude::*;
365
366        // IfcFacetedBrep attributes:
367        // 0: Outer (IfcClosedShell)
368
369        // Get closed shell ID
370        let shell_attr = entity
371            .get(0)
372            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
373
374        let shell_id = shell_attr
375            .as_entity_ref()
376            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
377
378        // FAST PATH: Get face IDs directly from ClosedShell raw bytes
379        let face_ids = decoder
380            .get_entity_ref_list_fast(shell_id)
381            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
382
383        // PHASE 1: Sequential - Extract all face data from IFC entities
384        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
385
386        for face_id in face_ids {
387            // FAST PATH: Get bound IDs directly from Face raw bytes
388            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
389                Some(ids) => ids,
390                None => continue,
391            };
392
393            // Separate outer bound from inner bounds (holes)
394            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
395            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
396
397            for bound_id in bound_ids {
398                // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
399                // get_face_bound_fast returns (loop_id, orientation, is_outer)
400                let (loop_id, orientation, is_outer) = match decoder.get_face_bound_fast(bound_id) {
401                    Some(data) => data,
402                    None => continue,
403                };
404
405                // FAST PATH: Get loop points directly from entity ID
406                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
407                    Some(p) => p,
408                    None => continue,
409                };
410
411                if !orientation {
412                    points.reverse();
413                }
414
415                if is_outer || outer_bound_points.is_none() {
416                    if outer_bound_points.is_some() && is_outer {
417                        if let Some(prev_outer) = outer_bound_points.take() {
418                            hole_points.push(prev_outer);
419                        }
420                    }
421                    outer_bound_points = Some(points);
422                } else {
423                    hole_points.push(points);
424                }
425            }
426
427            if let Some(outer_points) = outer_bound_points {
428                face_data_list.push(FaceData {
429                    outer_points,
430                    hole_points,
431                });
432            }
433        }
434
435        // PHASE 2: Triangulate all faces in parallel (via rayon on both native and WASM)
436        // Standard processor path uses no RTC (0,0,0) — the router applies RTC
437        // via transform_mesh. For full-precision infra models, the router calls
438        // process_with_rtc instead which passes the actual offset.
439        let face_results: Vec<FaceResult> = if face_data_list.len() < PAR_FACE_THRESHOLD {
440            face_data_list
441                .iter()
442                .map(|face| Self::triangulate_face(face, (0.0, 0.0, 0.0)))
443                .collect()
444        } else {
445            face_data_list
446                .par_iter()
447                .map(|face| Self::triangulate_face(face, (0.0, 0.0, 0.0)))
448                .collect()
449        };
450
451        // PHASE 3: Sequential - Merge all face results into final mesh
452        // Pre-calculate total sizes for efficient allocation
453        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
454        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
455
456        let mut positions = Vec::with_capacity(total_positions);
457        let mut indices = Vec::with_capacity(total_indices);
458
459        for result in face_results {
460            let base_idx = (positions.len() / 3) as u32;
461            positions.extend(result.positions);
462
463            // Offset indices by base
464            for idx in result.indices {
465                indices.push(base_idx + idx);
466            }
467        }
468
469        Ok(Mesh {
470            positions,
471            normals: Vec::new(),
472            indices,
473            rtc_applied: false, 
474            origin: [0.0; 3],        instance_meta: None, local_bounds: None, local_to_world: None })
475    }
476
477    fn supported_types(&self) -> Vec<IfcType> {
478        vec![IfcType::IfcFacetedBrep]
479    }
480}
481
482impl Default for FacetedBrepProcessor {
483    fn default() -> Self {
484        Self::new()
485    }
486}