Skip to main content

ifc_lite_geometry/processors/
brep.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//! BRep/surface model processors.
6//!
7//! Handles IfcFacetedBrep, IfcFaceBasedSurfaceModel, and IfcShellBasedSurfaceModel.
8//! All deal with boundary representations composed of face loops.
9
10use crate::{Error, Mesh, Point3, Result};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
12
13use super::advanced_face::process_advanced_face;
14use super::helpers::{extract_loop_points_by_id, FaceData, FaceResult};
15use crate::router::GeometryProcessor;
16
17// ---------- FacetedBrepProcessor ----------
18
19/// FacetedBrep processor
20/// Handles IfcFacetedBrep - explicit mesh with faces
21/// Supports faces with inner bounds (holes)
22/// Uses parallel triangulation for large BREPs
23pub struct FacetedBrepProcessor;
24
25impl FacetedBrepProcessor {
26    pub fn new() -> Self {
27        Self
28    }
29
30    /// Extract polygon points from a loop entity
31    /// Uses fast path for CartesianPoint extraction to avoid decode overhead
32    #[allow(dead_code)]
33    #[inline]
34    fn extract_loop_points(
35        &self,
36        loop_entity: &DecodedEntity,
37        decoder: &mut EntityDecoder,
38    ) -> Option<Vec<Point3<f64>>> {
39        // Try to get Polygon attribute (attribute 0) - IfcPolyLoop has this
40        let polygon_attr = loop_entity.get(0)?;
41
42        // Get the list of point references directly
43        let point_refs = polygon_attr.as_list()?;
44
45        // Pre-allocate with known size
46        let mut polygon_points = Vec::with_capacity(point_refs.len());
47
48        for point_ref in point_refs {
49            let point_id = point_ref.as_entity_ref()?;
50
51            // Try fast path first
52            if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
53                polygon_points.push(Point3::new(x, y, z));
54            } else {
55                // Fallback to standard path if fast extraction fails
56                let point = decoder.decode_by_id(point_id).ok()?;
57                let coords_attr = point.get(0)?;
58                let coords = coords_attr.as_list()?;
59                use ifc_lite_core::AttributeValue;
60                let x = coords.first().and_then(|v: &AttributeValue| v.as_float())?;
61                let y = coords.get(1).and_then(|v: &AttributeValue| v.as_float())?;
62                let z = coords.get(2).and_then(|v: &AttributeValue| v.as_float())?;
63                polygon_points.push(Point3::new(x, y, z));
64            }
65        }
66
67        if polygon_points.len() >= 3 {
68            Some(polygon_points)
69        } else {
70            None
71        }
72    }
73
74    /// Extract polygon points using ultra-fast path from loop entity ID
75    /// Uses cached coordinate extraction - points are cached across faces
76    /// This is the fastest path for files with shared cartesian points
77    #[inline]
78    fn extract_loop_points_fast(
79        &self,
80        loop_entity_id: u32,
81        decoder: &mut EntityDecoder,
82    ) -> Option<Vec<Point3<f64>>> {
83        // ULTRA-FAST PATH with CACHING: Get coordinates with point cache
84        // Many faces share the same cartesian points, so caching avoids
85        // re-parsing the same point data multiple times
86        let coords = decoder.get_polyloop_coords_cached(loop_entity_id)?;
87
88        // Convert to Point3 - pre-allocated in get_polyloop_coords_cached
89        let polygon_points: Vec<Point3<f64>> = coords
90            .into_iter()
91            .map(|(x, y, z)| Point3::new(x, y, z))
92            .collect();
93
94        if polygon_points.len() >= 3 {
95            Some(polygon_points)
96        } else {
97            None
98        }
99    }
100
101    /// Triangulate a single face (can be called in parallel)
102    /// Optimized with fast paths for simple faces.
103    ///
104    /// `rtc`: RTC offset subtracted from f64 coordinates BEFORE f32 conversion.
105    /// For infrastructure models with world-space coordinates (e.g. Y ≈ 6.2M),
106    /// f32 only has ~0.5m precision. Subtracting RTC in f64 first brings
107    /// values near origin where f32 has sub-mm precision, eliminating jitter.
108    #[inline]
109    fn triangulate_face(face: &FaceData, rtc: (f64, f64, f64)) -> FaceResult {
110        let n = face.outer_points.len();
111
112        // FAST PATH: Triangle without holes - no triangulation needed
113        if n == 3 && face.hole_points.is_empty() {
114            let mut positions = Vec::with_capacity(9);
115            for point in &face.outer_points {
116                positions.push((point.x - rtc.0) as f32);
117                positions.push((point.y - rtc.1) as f32);
118                positions.push((point.z - rtc.2) as f32);
119            }
120            return FaceResult {
121                positions,
122                indices: vec![0, 1, 2],
123            };
124        }
125
126        // FAST PATH: Quad without holes - simple fan
127        if n == 4 && face.hole_points.is_empty() {
128            let mut positions = Vec::with_capacity(12);
129            for point in &face.outer_points {
130                positions.push((point.x - rtc.0) as f32);
131                positions.push((point.y - rtc.1) as f32);
132                positions.push((point.z - rtc.2) as f32);
133            }
134            return FaceResult {
135                positions,
136                indices: vec![0, 1, 2, 0, 2, 3],
137            };
138        }
139
140        // FAST PATH: Simple convex polygon without holes
141        if face.hole_points.is_empty() && n <= 8 {
142            // Check if convex by testing cross products in 3D
143            let mut is_convex = true;
144            if n > 4 {
145                use crate::triangulation::calculate_polygon_normal;
146                let normal = calculate_polygon_normal(&face.outer_points);
147                let mut sign = 0i8;
148
149                for i in 0..n {
150                    let p0 = &face.outer_points[i];
151                    let p1 = &face.outer_points[(i + 1) % n];
152                    let p2 = &face.outer_points[(i + 2) % n];
153
154                    let v1 = p1 - p0;
155                    let v2 = p2 - p1;
156                    let cross = v1.cross(&v2);
157                    let dot = cross.dot(&normal);
158
159                    if dot.abs() > 1e-10 {
160                        let current_sign = if dot > 0.0 { 1i8 } else { -1i8 };
161                        if sign == 0 {
162                            sign = current_sign;
163                        } else if sign != current_sign {
164                            is_convex = false;
165                            break;
166                        }
167                    }
168                }
169            }
170
171            if is_convex {
172                let mut positions = Vec::with_capacity(n * 3);
173                for point in &face.outer_points {
174                    positions.push((point.x - rtc.0) as f32);
175                    positions.push((point.y - rtc.1) as f32);
176                    positions.push((point.z - rtc.2) as f32);
177                }
178                let mut indices = Vec::with_capacity((n - 2) * 3);
179                for i in 1..n - 1 {
180                    indices.push(0);
181                    indices.push(i as u32);
182                    indices.push(i as u32 + 1);
183                }
184                return FaceResult { positions, indices };
185            }
186        }
187
188        // SLOW PATH: Complex polygon or polygon with holes
189        use crate::triangulation::{
190            calculate_polygon_normal, project_to_2d, project_to_2d_with_basis,
191            triangulate_polygon_with_holes,
192        };
193
194        let mut positions = Vec::new();
195        let mut indices = Vec::new();
196
197        // Calculate face normal from outer boundary
198        let normal = calculate_polygon_normal(&face.outer_points);
199
200        // Project outer boundary to 2D and get the coordinate system
201        let (outer_2d, u_axis, v_axis, origin) = project_to_2d(&face.outer_points, &normal);
202
203        // Project holes to 2D using the SAME coordinate system as the outer boundary
204        let holes_2d: Vec<Vec<nalgebra::Point2<f64>>> = face
205            .hole_points
206            .iter()
207            .map(|hole| project_to_2d_with_basis(hole, &u_axis, &v_axis, &origin))
208            .collect();
209
210        // Triangulate with holes
211        let tri_indices = match triangulate_polygon_with_holes(&outer_2d, &holes_2d) {
212            Ok(idx) => idx,
213            Err(_) => {
214                // Fallback to simple fan triangulation without holes
215                for point in &face.outer_points {
216                    positions.push((point.x - rtc.0) as f32);
217                    positions.push((point.y - rtc.1) as f32);
218                    positions.push((point.z - rtc.2) as f32);
219                }
220                for i in 1..face.outer_points.len() - 1 {
221                    indices.push(0);
222                    indices.push(i as u32);
223                    indices.push(i as u32 + 1);
224                }
225                return FaceResult { positions, indices };
226            }
227        };
228
229        // Combine all 3D points (outer + holes) in the same order as 2D
230        let mut all_points_3d: Vec<&Point3<f64>> = face.outer_points.iter().collect();
231        for hole in &face.hole_points {
232            all_points_3d.extend(hole.iter());
233        }
234
235        // Add vertices (with RTC subtraction in f64 for full precision)
236        for point in &all_points_3d {
237            positions.push((point.x - rtc.0) as f32);
238            positions.push((point.y - rtc.1) as f32);
239            positions.push((point.z - rtc.2) as f32);
240        }
241
242        // Add triangle indices
243        for i in (0..tri_indices.len()).step_by(3) {
244            if i + 2 >= tri_indices.len() {
245                break;
246            }
247            indices.push(tri_indices[i] as u32);
248            indices.push(tri_indices[i + 1] as u32);
249            indices.push(tri_indices[i + 2] as u32);
250        }
251
252        FaceResult { positions, indices }
253    }
254
255    /// Batch process multiple FacetedBrep entities for maximum parallelism
256    /// Extracts all face data sequentially, then triangulates ALL faces in one parallel batch
257    /// Returns Vec of (brep_index, Mesh) pairs.
258    /// `rtc`: RTC offset applied during f64→f32 conversion for precision.
259    pub fn process_batch(
260        &self,
261        brep_ids: &[u32],
262        decoder: &mut EntityDecoder,
263        rtc: (f64, f64, f64),
264        large_coord_threshold_file_units: f64,
265    ) -> Vec<(usize, Mesh)> {
266        use rayon::prelude::*;
267
268        // PHASE 1: Sequential - Extract all face data from all BREPs
269        // Each entry: (brep_index, face_data)
270        let mut all_faces: Vec<(usize, FaceData)> = Vec::with_capacity(brep_ids.len() * 10);
271        let mut raw_large_by_brep = vec![false; brep_ids.len()];
272
273        for (brep_idx, &brep_id) in brep_ids.iter().enumerate() {
274            // FAST PATH: Get shell ID directly from raw bytes (avoids full entity decode)
275            let shell_id = match decoder.get_first_entity_ref_fast(brep_id) {
276                Some(id) => id,
277                None => continue,
278            };
279
280            // FAST PATH: Get face IDs from shell using raw bytes
281            let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
282                Some(ids) => ids,
283                None => continue,
284            };
285
286            // Extract face data for each face
287            for face_id in face_ids {
288                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
289                    Some(ids) => ids,
290                    None => continue,
291                };
292
293                let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
294                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
295
296                for bound_id in bound_ids {
297                    // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
298                    // get_face_bound_fast returns (loop_id, orientation, is_outer)
299                    let (loop_id, orientation, is_outer) =
300                        match decoder.get_face_bound_fast(bound_id) {
301                            Some(data) => data,
302                            None => continue,
303                        };
304
305                    // FAST PATH: Get loop points directly from entity ID
306                    let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
307                        Some(p) => p,
308                        None => continue,
309                    };
310
311                    if !raw_large_by_brep[brep_idx]
312                        && points.iter().any(|point| {
313                            point.x.abs().max(point.y.abs()).max(point.z.abs())
314                                > large_coord_threshold_file_units
315                        })
316                    {
317                        raw_large_by_brep[brep_idx] = true;
318                    }
319
320                    if !orientation {
321                        points.reverse();
322                    }
323
324                    if is_outer || outer_bound_points.is_none() {
325                        if outer_bound_points.is_some() && is_outer {
326                            if let Some(prev_outer) = outer_bound_points.take() {
327                                hole_points.push(prev_outer);
328                            }
329                        }
330                        outer_bound_points = Some(points);
331                    } else {
332                        hole_points.push(points);
333                    }
334                }
335
336                if let Some(outer_points) = outer_bound_points {
337                    all_faces.push((
338                        brep_idx,
339                        FaceData {
340                            outer_points,
341                            hole_points,
342                        },
343                    ));
344                }
345            }
346        }
347
348        // PHASE 2: Triangulate ALL faces from ALL BREPs in one parallel batch
349        // Uses rayon thread pool on both native and WASM (via wasm-bindgen-rayon)
350        let face_results: Vec<(usize, FaceResult)> = all_faces
351            .par_iter()
352            .map(|(brep_idx, face)| {
353                let face_rtc = if raw_large_by_brep[*brep_idx] {
354                    rtc
355                } else {
356                    (0.0, 0.0, 0.0)
357                };
358                (*brep_idx, Self::triangulate_face(face, face_rtc))
359            })
360            .collect();
361
362        // PHASE 3: Group results back by BREP index
363        // First, count faces per BREP to pre-allocate
364        let mut face_counts = vec![0usize; brep_ids.len()];
365        for (brep_idx, _) in &face_results {
366            face_counts[*brep_idx] += 1;
367        }
368
369        // Initialize mesh builders for each BREP
370        let mut mesh_builders: Vec<(Vec<f32>, Vec<u32>)> = face_counts
371            .iter()
372            .map(|&count| {
373                (
374                    Vec::with_capacity(count * 100),
375                    Vec::with_capacity(count * 50),
376                )
377            })
378            .collect();
379
380        // Merge face results into their respective meshes
381        for (brep_idx, result) in face_results {
382            let (positions, indices) = &mut mesh_builders[brep_idx];
383            let base_idx = (positions.len() / 3) as u32;
384            positions.extend(result.positions);
385            for idx in result.indices {
386                indices.push(base_idx + idx);
387            }
388        }
389
390        // Convert to final meshes
391        mesh_builders
392            .into_iter()
393            .enumerate()
394            .filter(|(_, (positions, _))| !positions.is_empty())
395            .map(|(brep_idx, (positions, indices))| {
396                (
397                    brep_idx,
398                    Mesh {
399                        positions,
400                        normals: Vec::new(),
401                        indices,
402                        // RTC is pre-subtracted only for raw world-coordinate Breps.
403                        rtc_applied: raw_large_by_brep[brep_idx]
404                            && (rtc.0 != 0.0 || rtc.1 != 0.0 || rtc.2 != 0.0),
405                    },
406                )
407            })
408            .collect()
409    }
410}
411
412impl FacetedBrepProcessor {
413    /// Process FacetedBrep with RTC offset applied during f64→f32 conversion.
414    /// This preserves sub-centimeter precision for infrastructure models where
415    /// vertices have large world coordinates (e.g. Y ≈ 6.2M meters).
416    pub fn process_with_rtc(
417        &self,
418        entity: &DecodedEntity,
419        decoder: &mut EntityDecoder,
420        _schema: &IfcSchema,
421        rtc: (f64, f64, f64),
422    ) -> Result<Mesh> {
423        use rayon::prelude::*;
424
425        let shell_attr = entity
426            .get(0)
427            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
428        let shell_id = shell_attr
429            .as_entity_ref()
430            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
431        let face_ids = decoder
432            .get_entity_ref_list_fast(shell_id)
433            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
434
435        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
436        for face_id in face_ids {
437            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
438                Some(ids) => ids,
439                None => continue,
440            };
441            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
442            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
443            for bound_id in bound_ids {
444                let (loop_id, orientation, is_outer) = match decoder.get_face_bound_fast(bound_id) {
445                    Some(data) => data,
446                    None => continue,
447                };
448                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
449                    Some(p) => p,
450                    None => continue,
451                };
452                if !orientation {
453                    points.reverse();
454                }
455                if is_outer || outer_bound_points.is_none() {
456                    if outer_bound_points.is_some() && is_outer {
457                        if let Some(prev_outer) = outer_bound_points.take() {
458                            hole_points.push(prev_outer);
459                        }
460                    }
461                    outer_bound_points = Some(points);
462                } else {
463                    hole_points.push(points);
464                }
465            }
466            if let Some(outer_points) = outer_bound_points {
467                face_data_list.push(FaceData {
468                    outer_points,
469                    hole_points,
470                });
471            }
472        }
473
474        let face_results: Vec<FaceResult> = face_data_list
475            .par_iter()
476            .map(|face| Self::triangulate_face(face, rtc))
477            .collect();
478
479        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
480        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
481        let mut positions = Vec::with_capacity(total_positions);
482        let mut indices = Vec::with_capacity(total_indices);
483        for result in face_results {
484            let base_idx = (positions.len() / 3) as u32;
485            positions.extend(result.positions);
486            for idx in result.indices {
487                indices.push(base_idx + idx);
488            }
489        }
490        Ok(Mesh {
491            positions,
492            normals: Vec::new(),
493            indices,
494            rtc_applied: true, // RTC already subtracted during f64→f32 conversion
495        })
496    }
497}
498
499impl GeometryProcessor for FacetedBrepProcessor {
500    fn process(
501        &self,
502        entity: &DecodedEntity,
503        decoder: &mut EntityDecoder,
504        _schema: &IfcSchema,
505    ) -> Result<Mesh> {
506        use rayon::prelude::*;
507
508        // IfcFacetedBrep attributes:
509        // 0: Outer (IfcClosedShell)
510
511        // Get closed shell ID
512        let shell_attr = entity
513            .get(0)
514            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
515
516        let shell_id = shell_attr
517            .as_entity_ref()
518            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
519
520        // FAST PATH: Get face IDs directly from ClosedShell raw bytes
521        let face_ids = decoder
522            .get_entity_ref_list_fast(shell_id)
523            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
524
525        // PHASE 1: Sequential - Extract all face data from IFC entities
526        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
527
528        for face_id in face_ids {
529            // FAST PATH: Get bound IDs directly from Face raw bytes
530            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
531                Some(ids) => ids,
532                None => continue,
533            };
534
535            // Separate outer bound from inner bounds (holes)
536            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
537            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
538
539            for bound_id in bound_ids {
540                // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
541                // get_face_bound_fast returns (loop_id, orientation, is_outer)
542                let (loop_id, orientation, is_outer) = match decoder.get_face_bound_fast(bound_id) {
543                    Some(data) => data,
544                    None => continue,
545                };
546
547                // FAST PATH: Get loop points directly from entity ID
548                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
549                    Some(p) => p,
550                    None => continue,
551                };
552
553                if !orientation {
554                    points.reverse();
555                }
556
557                if is_outer || outer_bound_points.is_none() {
558                    if outer_bound_points.is_some() && is_outer {
559                        if let Some(prev_outer) = outer_bound_points.take() {
560                            hole_points.push(prev_outer);
561                        }
562                    }
563                    outer_bound_points = Some(points);
564                } else {
565                    hole_points.push(points);
566                }
567            }
568
569            if let Some(outer_points) = outer_bound_points {
570                face_data_list.push(FaceData {
571                    outer_points,
572                    hole_points,
573                });
574            }
575        }
576
577        // PHASE 2: Triangulate all faces in parallel (via rayon on both native and WASM)
578        // Standard processor path uses no RTC (0,0,0) — the router applies RTC
579        // via transform_mesh. For full-precision infra models, the router calls
580        // process_with_rtc instead which passes the actual offset.
581        let face_results: Vec<FaceResult> = face_data_list
582            .par_iter()
583            .map(|face| Self::triangulate_face(face, (0.0, 0.0, 0.0)))
584            .collect();
585
586        // PHASE 3: Sequential - Merge all face results into final mesh
587        // Pre-calculate total sizes for efficient allocation
588        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
589        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
590
591        let mut positions = Vec::with_capacity(total_positions);
592        let mut indices = Vec::with_capacity(total_indices);
593
594        for result in face_results {
595            let base_idx = (positions.len() / 3) as u32;
596            positions.extend(result.positions);
597
598            // Offset indices by base
599            for idx in result.indices {
600                indices.push(base_idx + idx);
601            }
602        }
603
604        Ok(Mesh {
605            positions,
606            normals: Vec::new(),
607            indices,
608            rtc_applied: false,
609        })
610    }
611
612    fn supported_types(&self) -> Vec<IfcType> {
613        vec![IfcType::IfcFacetedBrep]
614    }
615}
616
617impl Default for FacetedBrepProcessor {
618    fn default() -> Self {
619        Self::new()
620    }
621}
622
623// ---------- FaceBasedSurfaceModelProcessor ----------
624
625/// FaceBasedSurfaceModel processor
626/// Handles IfcFaceBasedSurfaceModel - surface model made of connected face sets
627///
628/// Supports two face types within connected face sets:
629/// - Simple faces with IfcPolyLoop bounds (standard BRep from most exporters)
630/// - IfcAdvancedFace with B-spline/planar/cylindrical surfaces (CATIA, NURBS exports)
631///
632/// Structure (simple): FaceBasedSurfaceModel -> ConnectedFaceSet[] -> Face[] -> FaceBound -> PolyLoop
633/// Structure (advanced): FaceBasedSurfaceModel -> ConnectedFaceSet[] -> AdvancedFace[] -> FaceSurface
634pub struct FaceBasedSurfaceModelProcessor;
635
636impl FaceBasedSurfaceModelProcessor {
637    pub fn new() -> Self {
638        Self
639    }
640}
641
642impl GeometryProcessor for FaceBasedSurfaceModelProcessor {
643    fn process(
644        &self,
645        entity: &DecodedEntity,
646        decoder: &mut EntityDecoder,
647        _schema: &IfcSchema,
648    ) -> Result<Mesh> {
649        // IfcFaceBasedSurfaceModel attributes:
650        // 0: FbsmFaces (SET of IfcConnectedFaceSet)
651
652        let faces_attr = entity.get(0).ok_or_else(|| {
653            Error::geometry("FaceBasedSurfaceModel missing FbsmFaces".to_string())
654        })?;
655
656        let face_set_refs = faces_attr
657            .as_list()
658            .ok_or_else(|| Error::geometry("Expected face set list".to_string()))?;
659
660        let mut all_positions = Vec::new();
661        let mut all_indices = Vec::new();
662
663        // Process each connected face set
664        for face_set_ref in face_set_refs {
665            let face_set_id = face_set_ref.as_entity_ref().ok_or_else(|| {
666                Error::geometry("Expected entity reference for face set".to_string())
667            })?;
668
669            // Get face IDs from ConnectedFaceSet
670            let face_ids = match decoder.get_entity_ref_list_fast(face_set_id) {
671                Some(ids) => ids,
672                None => continue,
673            };
674
675            // Process each face in the set
676            for face_id in face_ids {
677                // Decode the face entity to check its type.
678                // Some exporters use IfcAdvancedFace within ConnectedFaceSet,
679                // which requires B-spline surface processing.
680                let face = match decoder.decode_by_id(face_id) {
681                    Ok(f) => f,
682                    Err(_) => continue,
683                };
684
685                if face.ifc_type == IfcType::IfcAdvancedFace {
686                    // Advanced face: delegate to shared NURBS/planar/cylindrical handler
687                    let (positions, indices) = match process_advanced_face(&face, decoder) {
688                        Ok(result) => result,
689                        Err(_) => continue,
690                    };
691
692                    if !positions.is_empty() {
693                        let base_idx = (all_positions.len() / 3) as u32;
694                        all_positions.extend(positions);
695                        for idx in indices {
696                            all_indices.push(base_idx + idx);
697                        }
698                    }
699                } else {
700                    // Simple face: extract PolyLoop points via fast path
701                    let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
702                        Some(ids) => ids,
703                        None => continue,
704                    };
705
706                    let mut outer_points: Option<Vec<Point3<f64>>> = None;
707                    let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
708
709                    for bound_id in bound_ids {
710                        // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
711                        // get_face_bound_fast returns (loop_id, orientation, is_outer)
712                        let (loop_id, orientation, is_outer) =
713                            match decoder.get_face_bound_fast(bound_id) {
714                                Some(data) => data,
715                                None => continue,
716                            };
717
718                        // Get loop points using shared helper
719                        let mut points = match extract_loop_points_by_id(loop_id, decoder) {
720                            Some(p) => p,
721                            None => continue,
722                        };
723
724                        if !orientation {
725                            points.reverse();
726                        }
727
728                        if is_outer || outer_points.is_none() {
729                            outer_points = Some(points);
730                        } else {
731                            hole_points.push(points);
732                        }
733                    }
734
735                    // Triangulate the face
736                    if let Some(outer) = outer_points {
737                        if outer.len() >= 3 {
738                            let base_idx = (all_positions.len() / 3) as u32;
739
740                            // Add positions
741                            for p in &outer {
742                                all_positions.push(p.x as f32);
743                                all_positions.push(p.y as f32);
744                                all_positions.push(p.z as f32);
745                            }
746
747                            // Simple fan triangulation (works for convex faces)
748                            for i in 1..outer.len() - 1 {
749                                all_indices.push(base_idx);
750                                all_indices.push(base_idx + i as u32);
751                                all_indices.push(base_idx + i as u32 + 1);
752                            }
753                        }
754                    }
755                }
756            }
757        }
758
759        Ok(Mesh {
760            positions: all_positions,
761            normals: Vec::new(),
762            indices: all_indices,
763            rtc_applied: false,
764        })
765    }
766
767    fn supported_types(&self) -> Vec<IfcType> {
768        vec![IfcType::IfcFaceBasedSurfaceModel]
769    }
770}
771
772impl Default for FaceBasedSurfaceModelProcessor {
773    fn default() -> Self {
774        Self::new()
775    }
776}
777
778// ---------- ShellBasedSurfaceModelProcessor ----------
779
780/// ShellBasedSurfaceModel processor
781/// Handles IfcShellBasedSurfaceModel - surface model made of shells
782///
783/// Supports two face types within shells:
784/// - Simple faces with IfcPolyLoop bounds (standard BRep from most exporters)
785/// - IfcAdvancedFace with B-spline/planar/cylindrical surfaces (CATIA, NURBS exports)
786///
787/// Structure (simple): ShellBasedSurfaceModel -> Shell[] -> Face[] -> FaceBound -> PolyLoop
788/// Structure (advanced): ShellBasedSurfaceModel -> Shell[] -> AdvancedFace[] -> FaceSurface
789pub struct ShellBasedSurfaceModelProcessor;
790
791impl ShellBasedSurfaceModelProcessor {
792    pub fn new() -> Self {
793        Self
794    }
795}
796
797impl GeometryProcessor for ShellBasedSurfaceModelProcessor {
798    fn process(
799        &self,
800        entity: &DecodedEntity,
801        decoder: &mut EntityDecoder,
802        _schema: &IfcSchema,
803    ) -> Result<Mesh> {
804        // IfcShellBasedSurfaceModel attributes:
805        // 0: SbsmBoundary (SET of IfcShell - either IfcOpenShell or IfcClosedShell)
806
807        let shells_attr = entity.get(0).ok_or_else(|| {
808            Error::geometry("ShellBasedSurfaceModel missing SbsmBoundary".to_string())
809        })?;
810
811        let shell_refs = shells_attr
812            .as_list()
813            .ok_or_else(|| Error::geometry("Expected shell list".to_string()))?;
814
815        let mut all_positions = Vec::new();
816        let mut all_indices = Vec::new();
817
818        // Process each shell
819        for shell_ref in shell_refs {
820            let shell_id = shell_ref.as_entity_ref().ok_or_else(|| {
821                Error::geometry("Expected entity reference for shell".to_string())
822            })?;
823
824            // Get face IDs from Shell (IfcOpenShell or IfcClosedShell)
825            // Both have CfsFaces as attribute 0
826            let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
827                Some(ids) => ids,
828                None => continue,
829            };
830
831            // Process each face in the shell
832            for face_id in face_ids {
833                // Decode the face entity to check its type.
834                // CATIA and other NURBS-based exporters use IfcAdvancedFace within
835                // IfcOpenShell/IfcClosedShell, which requires B-spline surface processing
836                // instead of simple PolyLoop extraction.
837                let face = match decoder.decode_by_id(face_id) {
838                    Ok(f) => f,
839                    Err(_) => continue,
840                };
841
842                if face.ifc_type == IfcType::IfcAdvancedFace {
843                    // Advanced face: delegate to shared NURBS/planar/cylindrical handler
844                    let (positions, indices) = match process_advanced_face(&face, decoder) {
845                        Ok(result) => result,
846                        Err(_) => continue,
847                    };
848
849                    if !positions.is_empty() {
850                        let base_idx = (all_positions.len() / 3) as u32;
851                        all_positions.extend(positions);
852                        for idx in indices {
853                            all_indices.push(base_idx + idx);
854                        }
855                    }
856                } else {
857                    // Simple face: extract PolyLoop points via fast path
858                    let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
859                        Some(ids) => ids,
860                        None => continue,
861                    };
862
863                    let mut outer_points: Option<Vec<Point3<f64>>> = None;
864                    let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
865
866                    for bound_id in bound_ids {
867                        // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
868                        let (loop_id, orientation, is_outer) =
869                            match decoder.get_face_bound_fast(bound_id) {
870                                Some(data) => data,
871                                None => continue,
872                            };
873
874                        // Get loop points using shared helper
875                        let mut points = match extract_loop_points_by_id(loop_id, decoder) {
876                            Some(p) => p,
877                            None => continue,
878                        };
879
880                        if !orientation {
881                            points.reverse();
882                        }
883
884                        if is_outer || outer_points.is_none() {
885                            outer_points = Some(points);
886                        } else {
887                            hole_points.push(points);
888                        }
889                    }
890
891                    // Triangulate the face
892                    if let Some(outer) = outer_points {
893                        if outer.len() >= 3 {
894                            let base_idx = (all_positions.len() / 3) as u32;
895
896                            // Add positions
897                            for p in &outer {
898                                all_positions.push(p.x as f32);
899                                all_positions.push(p.y as f32);
900                                all_positions.push(p.z as f32);
901                            }
902
903                            // Simple fan triangulation (works for convex faces)
904                            for i in 1..outer.len() - 1 {
905                                all_indices.push(base_idx);
906                                all_indices.push(base_idx + i as u32);
907                                all_indices.push(base_idx + i as u32 + 1);
908                            }
909                        }
910                    }
911                }
912            }
913        }
914
915        Ok(Mesh {
916            positions: all_positions,
917            normals: Vec::new(),
918            indices: all_indices,
919            rtc_applied: false,
920        })
921    }
922
923    fn supported_types(&self) -> Vec<IfcType> {
924        vec![IfcType::IfcShellBasedSurfaceModel]
925    }
926}
927
928impl Default for ShellBasedSurfaceModelProcessor {
929    fn default() -> Self {
930        Self::new()
931    }
932}