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            indices.push(tri_indices[i] as u32);
239            indices.push(tri_indices[i + 1] as u32);
240            indices.push(tri_indices[i + 2] as u32);
241        }
242
243        FaceResult { positions, indices }
244    }
245
246    /// Batch process multiple FacetedBrep entities for maximum parallelism
247    /// Extracts all face data sequentially, then triangulates ALL faces in one parallel batch
248    /// Returns Vec of (brep_index, Mesh) pairs
249    pub fn process_batch(
250        &self,
251        brep_ids: &[u32],
252        decoder: &mut EntityDecoder,
253    ) -> Vec<(usize, Mesh)> {
254        #[cfg(not(target_arch = "wasm32"))]
255        use rayon::prelude::*;
256
257        // PHASE 1: Sequential - Extract all face data from all BREPs
258        // Each entry: (brep_index, face_data)
259        let mut all_faces: Vec<(usize, FaceData)> = Vec::with_capacity(brep_ids.len() * 10);
260
261        for (brep_idx, &brep_id) in brep_ids.iter().enumerate() {
262            // FAST PATH: Get shell ID directly from raw bytes (avoids full entity decode)
263            let shell_id = match decoder.get_first_entity_ref_fast(brep_id) {
264                Some(id) => id,
265                None => continue,
266            };
267
268            // FAST PATH: Get face IDs from shell using raw bytes
269            let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
270                Some(ids) => ids,
271                None => continue,
272            };
273
274            // Extract face data for each face
275            for face_id in face_ids {
276                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
277                    Some(ids) => ids,
278                    None => continue,
279                };
280
281                let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
282                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
283
284                for bound_id in bound_ids {
285                    // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
286                    // get_face_bound_fast returns (loop_id, orientation, is_outer)
287                    let (loop_id, orientation, is_outer) =
288                        match decoder.get_face_bound_fast(bound_id) {
289                            Some(data) => data,
290                            None => continue,
291                        };
292
293                    // FAST PATH: Get loop points directly from entity ID
294                    let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
295                        Some(p) => p,
296                        None => continue,
297                    };
298
299                    if !orientation {
300                        points.reverse();
301                    }
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
315                if let Some(outer_points) = outer_bound_points {
316                    all_faces.push((
317                        brep_idx,
318                        FaceData {
319                            outer_points,
320                            hole_points,
321                        },
322                    ));
323                }
324            }
325        }
326
327        // PHASE 2: Triangulate ALL faces from ALL BREPs in one batch
328        // On native: use parallel iteration for multi-core speedup
329        // On WASM: use sequential iteration (no threads available, par_iter adds overhead)
330        #[cfg(not(target_arch = "wasm32"))]
331        let face_results: Vec<(usize, FaceResult)> = all_faces
332            .par_iter()
333            .map(|(brep_idx, face)| (*brep_idx, Self::triangulate_face(face)))
334            .collect();
335
336        #[cfg(target_arch = "wasm32")]
337        let face_results: Vec<(usize, FaceResult)> = all_faces
338            .iter()
339            .map(|(brep_idx, face)| (*brep_idx, Self::triangulate_face(face)))
340            .collect();
341
342        // PHASE 3: Group results back by BREP index
343        // First, count faces per BREP to pre-allocate
344        let mut face_counts = vec![0usize; brep_ids.len()];
345        for (brep_idx, _) in &face_results {
346            face_counts[*brep_idx] += 1;
347        }
348
349        // Initialize mesh builders for each BREP
350        let mut mesh_builders: Vec<(Vec<f32>, Vec<u32>)> = face_counts
351            .iter()
352            .map(|&count| {
353                (
354                    Vec::with_capacity(count * 100),
355                    Vec::with_capacity(count * 50),
356                )
357            })
358            .collect();
359
360        // Merge face results into their respective meshes
361        for (brep_idx, result) in face_results {
362            let (positions, indices) = &mut mesh_builders[brep_idx];
363            let base_idx = (positions.len() / 3) as u32;
364            positions.extend(result.positions);
365            for idx in result.indices {
366                indices.push(base_idx + idx);
367            }
368        }
369
370        // Convert to final meshes
371        mesh_builders
372            .into_iter()
373            .enumerate()
374            .filter(|(_, (positions, _))| !positions.is_empty())
375            .map(|(brep_idx, (positions, indices))| {
376                (
377                    brep_idx,
378                    Mesh {
379                        positions,
380                        normals: Vec::new(),
381                        indices,
382                    },
383                )
384            })
385            .collect()
386    }
387}
388
389impl GeometryProcessor for FacetedBrepProcessor {
390    fn process(
391        &self,
392        entity: &DecodedEntity,
393        decoder: &mut EntityDecoder,
394        _schema: &IfcSchema,
395    ) -> Result<Mesh> {
396        #[cfg(not(target_arch = "wasm32"))]
397        use rayon::prelude::*;
398
399        // IfcFacetedBrep attributes:
400        // 0: Outer (IfcClosedShell)
401
402        // Get closed shell ID
403        let shell_attr = entity
404            .get(0)
405            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
406
407        let shell_id = shell_attr
408            .as_entity_ref()
409            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
410
411        // FAST PATH: Get face IDs directly from ClosedShell raw bytes
412        let face_ids = decoder
413            .get_entity_ref_list_fast(shell_id)
414            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
415
416        // PHASE 1: Sequential - Extract all face data from IFC entities
417        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
418
419        for face_id in face_ids {
420            // FAST PATH: Get bound IDs directly from Face raw bytes
421            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
422                Some(ids) => ids,
423                None => continue,
424            };
425
426            // Separate outer bound from inner bounds (holes)
427            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
428            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
429
430            for bound_id in bound_ids {
431                // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
432                // get_face_bound_fast returns (loop_id, orientation, is_outer)
433                let (loop_id, orientation, is_outer) =
434                    match decoder.get_face_bound_fast(bound_id) {
435                        Some(data) => data,
436                        None => continue,
437                    };
438
439                // FAST PATH: Get loop points directly from entity ID
440                let mut points = match self.extract_loop_points_fast(loop_id, decoder) {
441                    Some(p) => p,
442                    None => continue,
443                };
444
445                if !orientation {
446                    points.reverse();
447                }
448
449                if is_outer || outer_bound_points.is_none() {
450                    if outer_bound_points.is_some() && is_outer {
451                        if let Some(prev_outer) = outer_bound_points.take() {
452                            hole_points.push(prev_outer);
453                        }
454                    }
455                    outer_bound_points = Some(points);
456                } else {
457                    hole_points.push(points);
458                }
459            }
460
461            if let Some(outer_points) = outer_bound_points {
462                face_data_list.push(FaceData {
463                    outer_points,
464                    hole_points,
465                });
466            }
467        }
468
469        // PHASE 2: Triangulate all faces
470        // On native: use parallel iteration for multi-core speedup
471        // On WASM: use sequential iteration (no threads available)
472        #[cfg(not(target_arch = "wasm32"))]
473        let face_results: Vec<FaceResult> = face_data_list
474            .par_iter()
475            .map(Self::triangulate_face)
476            .collect();
477
478        #[cfg(target_arch = "wasm32")]
479        let face_results: Vec<FaceResult> = face_data_list
480            .iter()
481            .map(Self::triangulate_face)
482            .collect();
483
484        // PHASE 3: Sequential - Merge all face results into final mesh
485        // Pre-calculate total sizes for efficient allocation
486        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
487        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
488
489        let mut positions = Vec::with_capacity(total_positions);
490        let mut indices = Vec::with_capacity(total_indices);
491
492        for result in face_results {
493            let base_idx = (positions.len() / 3) as u32;
494            positions.extend(result.positions);
495
496            // Offset indices by base
497            for idx in result.indices {
498                indices.push(base_idx + idx);
499            }
500        }
501
502        Ok(Mesh {
503            positions,
504            normals: Vec::new(),
505            indices,
506        })
507    }
508
509    fn supported_types(&self) -> Vec<IfcType> {
510        vec![IfcType::IfcFacetedBrep]
511    }
512}
513
514impl Default for FacetedBrepProcessor {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520// ---------- FaceBasedSurfaceModelProcessor ----------
521
522/// FaceBasedSurfaceModel processor
523/// Handles IfcFaceBasedSurfaceModel - surface model made of connected face sets
524/// Structure: FaceBasedSurfaceModel -> ConnectedFaceSet[] -> Face[] -> FaceBound -> PolyLoop
525pub struct FaceBasedSurfaceModelProcessor;
526
527impl FaceBasedSurfaceModelProcessor {
528    pub fn new() -> Self {
529        Self
530    }
531}
532
533impl GeometryProcessor for FaceBasedSurfaceModelProcessor {
534    fn process(
535        &self,
536        entity: &DecodedEntity,
537        decoder: &mut EntityDecoder,
538        _schema: &IfcSchema,
539    ) -> Result<Mesh> {
540        // IfcFaceBasedSurfaceModel attributes:
541        // 0: FbsmFaces (SET of IfcConnectedFaceSet)
542
543        let faces_attr = entity
544            .get(0)
545            .ok_or_else(|| Error::geometry("FaceBasedSurfaceModel missing FbsmFaces".to_string()))?;
546
547        let face_set_refs = faces_attr
548            .as_list()
549            .ok_or_else(|| Error::geometry("Expected face set list".to_string()))?;
550
551        let mut all_positions = Vec::new();
552        let mut all_indices = Vec::new();
553
554        // Process each connected face set
555        for face_set_ref in face_set_refs {
556            let face_set_id = face_set_ref.as_entity_ref().ok_or_else(|| {
557                Error::geometry("Expected entity reference for face set".to_string())
558            })?;
559
560            // Get face IDs from ConnectedFaceSet
561            let face_ids = match decoder.get_entity_ref_list_fast(face_set_id) {
562                Some(ids) => ids,
563                None => continue,
564            };
565
566            // Process each face in the set
567            for face_id in face_ids {
568                // Get bound IDs from Face
569                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
570                    Some(ids) => ids,
571                    None => continue,
572                };
573
574                let mut outer_points: Option<Vec<Point3<f64>>> = None;
575                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
576
577                for bound_id in bound_ids {
578                    // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
579                    // get_face_bound_fast returns (loop_id, orientation, is_outer)
580                    let (loop_id, orientation, is_outer) =
581                        match decoder.get_face_bound_fast(bound_id) {
582                            Some(data) => data,
583                            None => continue,
584                        };
585
586                    // Get loop points using shared helper
587                    let mut points = match extract_loop_points_by_id(loop_id, decoder) {
588                        Some(p) => p,
589                        None => continue,
590                    };
591
592                    if !orientation {
593                        points.reverse();
594                    }
595
596                    if is_outer || outer_points.is_none() {
597                        outer_points = Some(points);
598                    } else {
599                        hole_points.push(points);
600                    }
601                }
602
603                // Triangulate the face
604                if let Some(outer) = outer_points {
605                    if outer.len() >= 3 {
606                        let base_idx = (all_positions.len() / 3) as u32;
607
608                        // Add positions
609                        for p in &outer {
610                            all_positions.push(p.x as f32);
611                            all_positions.push(p.y as f32);
612                            all_positions.push(p.z as f32);
613                        }
614
615                        // Simple fan triangulation (works for convex faces)
616                        for i in 1..outer.len() - 1 {
617                            all_indices.push(base_idx);
618                            all_indices.push(base_idx + i as u32);
619                            all_indices.push(base_idx + i as u32 + 1);
620                        }
621                    }
622                }
623            }
624        }
625
626        Ok(Mesh {
627            positions: all_positions,
628            normals: Vec::new(),
629            indices: all_indices,
630        })
631    }
632
633    fn supported_types(&self) -> Vec<IfcType> {
634        vec![IfcType::IfcFaceBasedSurfaceModel]
635    }
636}
637
638impl Default for FaceBasedSurfaceModelProcessor {
639    fn default() -> Self {
640        Self::new()
641    }
642}
643
644// ---------- ShellBasedSurfaceModelProcessor ----------
645
646/// ShellBasedSurfaceModel processor
647/// Handles IfcShellBasedSurfaceModel - surface model made of shells
648/// Structure: ShellBasedSurfaceModel -> Shell[] -> Face[] -> FaceBound -> PolyLoop
649pub struct ShellBasedSurfaceModelProcessor;
650
651impl ShellBasedSurfaceModelProcessor {
652    pub fn new() -> Self {
653        Self
654    }
655}
656
657impl GeometryProcessor for ShellBasedSurfaceModelProcessor {
658    fn process(
659        &self,
660        entity: &DecodedEntity,
661        decoder: &mut EntityDecoder,
662        _schema: &IfcSchema,
663    ) -> Result<Mesh> {
664        // IfcShellBasedSurfaceModel attributes:
665        // 0: SbsmBoundary (SET of IfcShell - either IfcOpenShell or IfcClosedShell)
666
667        let shells_attr = entity
668            .get(0)
669            .ok_or_else(|| Error::geometry("ShellBasedSurfaceModel missing SbsmBoundary".to_string()))?;
670
671        let shell_refs = shells_attr
672            .as_list()
673            .ok_or_else(|| Error::geometry("Expected shell list".to_string()))?;
674
675        let mut all_positions = Vec::new();
676        let mut all_indices = Vec::new();
677
678        // Process each shell
679        for shell_ref in shell_refs {
680            let shell_id = shell_ref.as_entity_ref().ok_or_else(|| {
681                Error::geometry("Expected entity reference for shell".to_string())
682            })?;
683
684            // Get face IDs from Shell (IfcOpenShell or IfcClosedShell)
685            // Both have CfsFaces as attribute 0
686            let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
687                Some(ids) => ids,
688                None => continue,
689            };
690
691            // Process each face in the shell
692            for face_id in face_ids {
693                // Get bound IDs from Face
694                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
695                    Some(ids) => ids,
696                    None => continue,
697                };
698
699                let mut outer_points: Option<Vec<Point3<f64>>> = None;
700                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
701
702                for bound_id in bound_ids {
703                    // FAST PATH: Extract loop_id, orientation, is_outer from raw bytes
704                    let (loop_id, orientation, is_outer) =
705                        match decoder.get_face_bound_fast(bound_id) {
706                            Some(data) => data,
707                            None => continue,
708                        };
709
710                    // Get loop points using shared helper
711                    let mut points = match extract_loop_points_by_id(loop_id, decoder) {
712                        Some(p) => p,
713                        None => continue,
714                    };
715
716                    if !orientation {
717                        points.reverse();
718                    }
719
720                    if is_outer || outer_points.is_none() {
721                        outer_points = Some(points);
722                    } else {
723                        hole_points.push(points);
724                    }
725                }
726
727                // Triangulate the face
728                if let Some(outer) = outer_points {
729                    if outer.len() >= 3 {
730                        let base_idx = (all_positions.len() / 3) as u32;
731
732                        // Add positions
733                        for p in &outer {
734                            all_positions.push(p.x as f32);
735                            all_positions.push(p.y as f32);
736                            all_positions.push(p.z as f32);
737                        }
738
739                        // Simple fan triangulation (works for convex faces)
740                        for i in 1..outer.len() - 1 {
741                            all_indices.push(base_idx);
742                            all_indices.push(base_idx + i as u32);
743                            all_indices.push(base_idx + i as u32 + 1);
744                        }
745                    }
746                }
747            }
748        }
749
750        Ok(Mesh {
751            positions: all_positions,
752            normals: Vec::new(),
753            indices: all_indices,
754        })
755    }
756
757    fn supported_types(&self) -> Vec<IfcType> {
758        vec![IfcType::IfcShellBasedSurfaceModel]
759    }
760}
761
762impl Default for ShellBasedSurfaceModelProcessor {
763    fn default() -> Self {
764        Self::new()
765    }
766}