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