ifc_lite_geometry/
processors.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//! Geometry Processors - P0 implementations
6//!
7//! High-priority processors for common IFC geometry types.
8
9use crate::{
10    extrusion::{apply_transform, extrude_profile},
11    profiles::ProfileProcessor,
12    Error, Mesh, Point3, Result, Vector3,
13};
14use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
15use nalgebra::Matrix4;
16
17use super::router::GeometryProcessor;
18
19/// Extract CoordIndex bytes from IfcTriangulatedFaceSet raw entity
20///
21/// Finds the 4th attribute (CoordIndex, 0-indexed as 3) in:
22/// `#77=IFCTRIANGULATEDFACESET(#78,$,$,((1,2,3),(2,1,4),...),$);`
23///
24/// Returns the byte slice containing just the index list data.
25/// Performs structural validation to reject malformed input.
26#[inline]
27fn extract_coord_index_bytes(bytes: &[u8]) -> Option<&[u8]> {
28    // Find opening paren after = sign
29    let eq_pos = bytes.iter().position(|&b| b == b'=')?;
30    let open_paren = bytes[eq_pos..].iter().position(|&b| b == b'(')?;
31    let args_start = eq_pos + open_paren + 1;
32
33    // Navigate through attributes counting at depth 1
34    let mut depth = 1;
35    let mut attr_count = 0;
36    let mut attr_start = args_start;
37    let mut i = args_start;
38    let mut in_string = false;
39
40    while i < bytes.len() && depth > 0 {
41        let b = bytes[i];
42
43        // Handle string literals - skip content inside quotes
44        if b == b'\'' {
45            in_string = !in_string;
46            i += 1;
47            continue;
48        }
49        if in_string {
50            i += 1;
51            continue;
52        }
53
54        // Skip comments (/* ... */)
55        if b == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
56            i += 2;
57            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
58                i += 1;
59            }
60            i += 2;
61            continue;
62        }
63
64        match b {
65            b'(' => {
66                if depth == 1 && attr_count == 3 {
67                    // Found start of 4th attribute (CoordIndex)
68                    attr_start = i;
69                }
70                depth += 1;
71            }
72            b')' => {
73                depth -= 1;
74                if depth == 1 && attr_count == 3 {
75                    // Found end of CoordIndex - validate before returning
76                    let candidate = &bytes[attr_start..i + 1];
77                    if validate_coord_index_structure(candidate) {
78                        return Some(candidate);
79                    }
80                    // Invalid structure, continue searching or return None
81                    return None;
82                }
83            }
84            b',' if depth == 1 => {
85                attr_count += 1;
86            }
87            b'$' if depth == 1 && attr_count == 3 => {
88                // CoordIndex is $ (null), skip it
89                return None;
90            }
91            _ => {}
92        }
93        i += 1;
94    }
95
96    None
97}
98
99/// Validate that a byte slice has valid CoordIndex structure:
100/// - Must start with '(' and end with ')'
101/// - Must contain comma-separated parenthesized integer lists
102/// - Allowed tokens: digits, commas, parentheses, whitespace
103/// - Rejected: '$', unbalanced parens, quotes, comment markers
104#[inline]
105fn validate_coord_index_structure(bytes: &[u8]) -> bool {
106    if bytes.is_empty() {
107        return false;
108    }
109
110    // Must start with '(' and end with ')'
111    let first = bytes.first().copied();
112    let last = bytes.last().copied();
113    if first != Some(b'(') || last != Some(b')') {
114        return false;
115    }
116
117    // Check structure: only allow digits, commas, parens, whitespace
118    let mut depth = 0;
119    for &b in bytes {
120        match b {
121            b'(' => depth += 1,
122            b')' => {
123                if depth == 0 {
124                    return false; // Unbalanced
125                }
126                depth -= 1;
127            }
128            b'0'..=b'9' | b',' | b' ' | b'\t' | b'\n' | b'\r' | b'-' => {}
129            b'$' | b'\'' | b'"' | b'/' | b'*' | b'#' => {
130                // Invalid characters for CoordIndex
131                return false;
132            }
133            _ => {
134                // Allow other whitespace-like chars, reject letters
135                if b.is_ascii_alphabetic() {
136                    return false;
137                }
138            }
139        }
140    }
141
142    // Must have balanced parens
143    depth == 0
144}
145
146/// ExtrudedAreaSolid processor (P0)
147/// Handles IfcExtrudedAreaSolid - extrusion of 2D profiles
148pub struct ExtrudedAreaSolidProcessor {
149    profile_processor: ProfileProcessor,
150}
151
152impl ExtrudedAreaSolidProcessor {
153    /// Create new processor
154    pub fn new(schema: IfcSchema) -> Self {
155        Self {
156            profile_processor: ProfileProcessor::new(schema),
157        }
158    }
159}
160
161impl GeometryProcessor for ExtrudedAreaSolidProcessor {
162    fn process(
163        &self,
164        entity: &DecodedEntity,
165        decoder: &mut EntityDecoder,
166        _schema: &IfcSchema,
167    ) -> Result<Mesh> {
168        // IfcExtrudedAreaSolid attributes:
169        // 0: SweptArea (IfcProfileDef)
170        // 1: Position (IfcAxis2Placement3D)
171        // 2: ExtrudedDirection (IfcDirection)
172        // 3: Depth (IfcPositiveLengthMeasure)
173
174        // Get profile
175        let profile_attr = entity
176            .get(0)
177            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea".to_string()))?;
178
179        let profile_entity = decoder
180            .resolve_ref(profile_attr)?
181            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
182
183        let profile = self.profile_processor.process(&profile_entity, decoder)?;
184
185        if profile.outer.is_empty() {
186            return Ok(Mesh::new());
187        }
188
189        // Get extrusion direction
190        let direction_attr = entity.get(2).ok_or_else(|| {
191            Error::geometry("ExtrudedAreaSolid missing ExtrudedDirection".to_string())
192        })?;
193
194        let direction_entity = decoder
195            .resolve_ref(direction_attr)?
196            .ok_or_else(|| Error::geometry("Failed to resolve ExtrudedDirection".to_string()))?;
197
198        if direction_entity.ifc_type != IfcType::IfcDirection {
199            return Err(Error::geometry(format!(
200                "Expected IfcDirection, got {}",
201                direction_entity.ifc_type
202            )));
203        }
204
205        // Parse direction
206        let ratios_attr = direction_entity
207            .get(0)
208            .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
209
210        let ratios = ratios_attr
211            .as_list()
212            .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
213
214        use ifc_lite_core::AttributeValue;
215        let dir_x = ratios
216            .first()
217            .and_then(|v: &AttributeValue| v.as_float())
218            .unwrap_or(0.0);
219        let dir_y = ratios
220            .get(1)
221            .and_then(|v: &AttributeValue| v.as_float())
222            .unwrap_or(0.0);
223        let dir_z = ratios
224            .get(2)
225            .and_then(|v: &AttributeValue| v.as_float())
226            .unwrap_or(1.0);
227
228        let direction = Vector3::new(dir_x, dir_y, dir_z).normalize();
229
230        // Get depth
231        let depth = entity
232            .get_float(3)
233            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing Depth".to_string()))?;
234
235        // ExtrudedDirection is in the local coordinate system defined by Position.
236        // Most IFC files use (0,0,1) or (0,0,-1) for vertical extrusions.
237        // We should NOT apply a separate rotation for the extrusion direction when it's
238        // aligned with Z, as Position will handle the full orientation.
239        // Only apply additional rotation when ExtrudedDirection has X or Y components
240        // (i.e., non-vertical extrusion in local space).
241        let transform = if direction.x.abs() < 0.001 && direction.y.abs() < 0.001 {
242            // ExtrudedDirection is along local Z axis - no additional rotation needed
243            // Position transform will handle the orientation
244            //
245            // However, if ExtrudedDirection is (0,0,-1), we need to offset the extrusion
246            // so it goes from Z=-depth to Z=0 instead of Z=0 to Z=+depth.
247            // This is because extrude_profile always extrudes in +Z, but IFC expects
248            // the solid to extend in the ExtrudedDirection from the profile plane.
249            if direction.z < 0.0 {
250                // Shift the extrusion down by depth so it extends in -Z from the profile plane
251                Some(Matrix4::new_translation(&Vector3::new(0.0, 0.0, -depth)))
252            } else {
253                None
254            }
255        } else {
256            // Non-Z-aligned extrusion: construct rotation to align with extrusion direction
257            let new_z = direction.normalize();
258
259            // Choose up vector (world Z, unless direction is nearly vertical)
260            let up = if new_z.z.abs() > 0.9 {
261                Vector3::new(0.0, 1.0, 0.0) // Use Y when nearly vertical
262            } else {
263                Vector3::new(0.0, 0.0, 1.0) // Use Z otherwise
264            };
265
266            let new_x = up.cross(&new_z).normalize();
267            let new_y = new_z.cross(&new_x).normalize();
268
269            let mut transform_mat = Matrix4::identity();
270            transform_mat[(0, 0)] = new_x.x;
271            transform_mat[(1, 0)] = new_x.y;
272            transform_mat[(2, 0)] = new_x.z;
273            transform_mat[(0, 1)] = new_y.x;
274            transform_mat[(1, 1)] = new_y.y;
275            transform_mat[(2, 1)] = new_y.z;
276            transform_mat[(0, 2)] = new_z.x;
277            transform_mat[(1, 2)] = new_z.y;
278            transform_mat[(2, 2)] = new_z.z;
279
280            Some(transform_mat)
281        };
282
283        // Extrude the profile
284        let mut mesh = extrude_profile(&profile, depth, transform)?;
285
286        // Apply Position transform (attribute 1: IfcAxis2Placement3D)
287        if let Some(pos_attr) = entity.get(1) {
288            if !pos_attr.is_null() {
289                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
290                    if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
291                        let pos_transform = self.parse_axis2_placement_3d(&pos_entity, decoder)?;
292                        apply_transform(&mut mesh, &pos_transform);
293                    }
294                }
295            }
296        }
297
298        Ok(mesh)
299    }
300
301    fn supported_types(&self) -> Vec<IfcType> {
302        vec![IfcType::IfcExtrudedAreaSolid]
303    }
304}
305
306impl ExtrudedAreaSolidProcessor {
307    /// Parse IfcAxis2Placement3D into transformation matrix
308    #[inline]
309    fn parse_axis2_placement_3d(
310        &self,
311        placement: &DecodedEntity,
312        decoder: &mut EntityDecoder,
313    ) -> Result<Matrix4<f64>> {
314        // IfcAxis2Placement3D: Location, Axis, RefDirection
315        let location = self.parse_cartesian_point(placement, decoder, 0)?;
316
317        // Default axes if not specified
318        let z_axis = if let Some(axis_attr) = placement.get(1) {
319            if !axis_attr.is_null() {
320                if let Some(axis_entity) = decoder.resolve_ref(axis_attr)? {
321                    self.parse_direction(&axis_entity)?
322                } else {
323                    Vector3::new(0.0, 0.0, 1.0)
324                }
325            } else {
326                Vector3::new(0.0, 0.0, 1.0)
327            }
328        } else {
329            Vector3::new(0.0, 0.0, 1.0)
330        };
331
332        let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
333            if !ref_dir_attr.is_null() {
334                if let Some(ref_dir_entity) = decoder.resolve_ref(ref_dir_attr)? {
335                    self.parse_direction(&ref_dir_entity)?
336                } else {
337                    Vector3::new(1.0, 0.0, 0.0)
338                }
339            } else {
340                Vector3::new(1.0, 0.0, 0.0)
341            }
342        } else {
343            Vector3::new(1.0, 0.0, 0.0)
344        };
345
346        // Normalize axes
347        let z_axis_final = z_axis.normalize();
348        let x_axis_normalized = x_axis.normalize();
349
350        // Ensure X is orthogonal to Z (project X onto plane perpendicular to Z)
351        let dot_product = x_axis_normalized.dot(&z_axis_final);
352        let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
353        let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
354            x_axis_orthogonal.normalize()
355        } else {
356            // X and Z are parallel or nearly parallel - use a default perpendicular direction
357            if z_axis_final.z.abs() < 0.9 {
358                Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
359            } else {
360                Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
361            }
362        };
363
364        // Y axis is cross product of Z and X (right-hand rule: Y = Z × X)
365        let y_axis = z_axis_final.cross(&x_axis_final).normalize();
366
367        // Build transformation matrix
368        // Columns represent world-space directions of local axes
369        let mut transform = Matrix4::identity();
370        transform[(0, 0)] = x_axis_final.x;
371        transform[(1, 0)] = x_axis_final.y;
372        transform[(2, 0)] = x_axis_final.z;
373        transform[(0, 1)] = y_axis.x;
374        transform[(1, 1)] = y_axis.y;
375        transform[(2, 1)] = y_axis.z;
376        transform[(0, 2)] = z_axis_final.x;
377        transform[(1, 2)] = z_axis_final.y;
378        transform[(2, 2)] = z_axis_final.z;
379        transform[(0, 3)] = location.x;
380        transform[(1, 3)] = location.y;
381        transform[(2, 3)] = location.z;
382
383        Ok(transform)
384    }
385
386    /// Parse IfcCartesianPoint
387    #[inline]
388    fn parse_cartesian_point(
389        &self,
390        parent: &DecodedEntity,
391        decoder: &mut EntityDecoder,
392        attr_index: usize,
393    ) -> Result<Point3<f64>> {
394        let point_attr = parent
395            .get(attr_index)
396            .ok_or_else(|| Error::geometry("Missing cartesian point".to_string()))?;
397
398        let point_entity = decoder
399            .resolve_ref(point_attr)?
400            .ok_or_else(|| Error::geometry("Failed to resolve cartesian point".to_string()))?;
401
402        if point_entity.ifc_type != IfcType::IfcCartesianPoint {
403            return Err(Error::geometry(format!(
404                "Expected IfcCartesianPoint, got {}",
405                point_entity.ifc_type
406            )));
407        }
408
409        // Get coordinates list (attribute 0)
410        let coords_attr = point_entity
411            .get(0)
412            .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
413
414        let coords = coords_attr
415            .as_list()
416            .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
417
418        let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
419        let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
420        let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
421
422        Ok(Point3::new(x, y, z))
423    }
424
425    /// Parse IfcDirection
426    #[inline]
427    fn parse_direction(&self, direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
428        if direction_entity.ifc_type != IfcType::IfcDirection {
429            return Err(Error::geometry(format!(
430                "Expected IfcDirection, got {}",
431                direction_entity.ifc_type
432            )));
433        }
434
435        // Get direction ratios (attribute 0)
436        let ratios_attr = direction_entity
437            .get(0)
438            .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
439
440        let ratios = ratios_attr
441            .as_list()
442            .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
443
444        let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
445        let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
446        let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
447
448        Ok(Vector3::new(x, y, z))
449    }
450}
451
452/// TriangulatedFaceSet processor (P0)
453/// Handles IfcTriangulatedFaceSet - explicit triangle meshes
454pub struct TriangulatedFaceSetProcessor;
455
456impl TriangulatedFaceSetProcessor {
457    pub fn new() -> Self {
458        Self
459    }
460}
461
462impl GeometryProcessor for TriangulatedFaceSetProcessor {
463    #[inline]
464    fn process(
465        &self,
466        entity: &DecodedEntity,
467        decoder: &mut EntityDecoder,
468        _schema: &IfcSchema,
469    ) -> Result<Mesh> {
470        // IfcTriangulatedFaceSet attributes:
471        // 0: Coordinates (IfcCartesianPointList3D)
472        // 1: Normals (optional)
473        // 2: Closed (optional)
474        // 3: CoordIndex (list of list of IfcPositiveInteger)
475
476        // Get coordinate entity reference
477        let coords_attr = entity.get(0).ok_or_else(|| {
478            Error::geometry("TriangulatedFaceSet missing Coordinates".to_string())
479        })?;
480
481        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
482            Error::geometry("Expected entity reference for Coordinates".to_string())
483        })?;
484
485        // FAST PATH: Try direct parsing of raw bytes (3-5x faster)
486        // This bypasses Token/AttributeValue allocations entirely
487        use ifc_lite_core::{extract_coordinate_list_from_entity, parse_indices_direct};
488
489        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
490            // Fast path: parse coordinates directly from raw bytes
491            // Use extract_coordinate_list_from_entity to skip entity header (#N=IFCTYPE...)
492            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
493        } else {
494            // Fallback path: use standard decoding
495            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
496
497            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
498                Error::geometry("CartesianPointList3D missing CoordList".to_string())
499            })?;
500
501            let coord_list = coord_list_attr
502                .as_list()
503                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
504
505            use ifc_lite_core::AttributeValue;
506            AttributeValue::parse_coordinate_list_3d(coord_list)
507        };
508
509        // Get face indices - try fast path first
510        let indices_attr = entity
511            .get(3)
512            .ok_or_else(|| Error::geometry("TriangulatedFaceSet missing CoordIndex".to_string()))?;
513
514        // For indices, we need to extract from the main entity's raw bytes
515        // Fast path: parse directly if we can get the raw CoordIndex section
516        let indices = if let Some(raw_entity_bytes) = decoder.get_raw_bytes(entity.id) {
517            // Find the CoordIndex attribute (4th attribute, index 3)
518            // and parse directly
519            if let Some(coord_index_bytes) = extract_coord_index_bytes(raw_entity_bytes) {
520                parse_indices_direct(coord_index_bytes)
521            } else {
522                // Fallback to standard parsing
523                let face_list = indices_attr
524                    .as_list()
525                    .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
526                use ifc_lite_core::AttributeValue;
527                AttributeValue::parse_index_list(face_list)
528            }
529        } else {
530            let face_list = indices_attr
531                .as_list()
532                .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
533            use ifc_lite_core::AttributeValue;
534            AttributeValue::parse_index_list(face_list)
535        };
536
537        // Create mesh (normals will be computed later)
538        Ok(Mesh {
539            positions,
540            normals: Vec::new(),
541            indices,
542        })
543    }
544
545    fn supported_types(&self) -> Vec<IfcType> {
546        vec![IfcType::IfcTriangulatedFaceSet]
547    }
548}
549
550impl Default for TriangulatedFaceSetProcessor {
551    fn default() -> Self {
552        Self::new()
553    }
554}
555
556/// Face data extracted from IFC for parallel triangulation
557struct FaceData {
558    outer_points: Vec<Point3<f64>>,
559    hole_points: Vec<Vec<Point3<f64>>>,
560}
561
562/// Triangulated face result
563struct FaceResult {
564    positions: Vec<f32>,
565    indices: Vec<u32>,
566}
567
568/// FacetedBrep processor
569/// Handles IfcFacetedBrep - explicit mesh with faces
570/// Supports faces with inner bounds (holes)
571/// Uses parallel triangulation for large BREPs
572pub struct FacetedBrepProcessor;
573
574impl FacetedBrepProcessor {
575    pub fn new() -> Self {
576        Self
577    }
578
579    /// Extract polygon points from a loop entity
580    /// Uses fast path for CartesianPoint extraction to avoid decode overhead
581    #[allow(dead_code)]
582    #[inline]
583    fn extract_loop_points(
584        &self,
585        loop_entity: &DecodedEntity,
586        decoder: &mut EntityDecoder,
587    ) -> Option<Vec<Point3<f64>>> {
588        // Try to get Polygon attribute (attribute 0) - IfcPolyLoop has this
589        let polygon_attr = loop_entity.get(0)?;
590
591        // Get the list of point references directly
592        let point_refs = polygon_attr.as_list()?;
593
594        // Pre-allocate with known size
595        let mut polygon_points = Vec::with_capacity(point_refs.len());
596
597        for point_ref in point_refs {
598            let point_id = point_ref.as_entity_ref()?;
599
600            // Try fast path first
601            if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
602                polygon_points.push(Point3::new(x, y, z));
603            } else {
604                // Fallback to standard path if fast extraction fails
605                let point = decoder.decode_by_id(point_id).ok()?;
606                let coords_attr = point.get(0)?;
607                let coords = coords_attr.as_list()?;
608                use ifc_lite_core::AttributeValue;
609                let x = coords.first().and_then(|v: &AttributeValue| v.as_float())?;
610                let y = coords.get(1).and_then(|v: &AttributeValue| v.as_float())?;
611                let z = coords.get(2).and_then(|v: &AttributeValue| v.as_float())?;
612                polygon_points.push(Point3::new(x, y, z));
613            }
614        }
615
616        if polygon_points.len() >= 3 {
617            Some(polygon_points)
618        } else {
619            None
620        }
621    }
622
623    /// Extract polygon points using fast path from loop entity ID
624    /// Bypasses full entity decoding for ~2x speedup on BREP-heavy files
625    #[inline]
626    fn extract_loop_points_fast(
627        &self,
628        loop_entity_id: u32,
629        decoder: &mut EntityDecoder,
630    ) -> Option<Vec<Point3<f64>>> {
631        // Get point IDs directly from raw bytes
632        let point_ids = decoder.get_polyloop_point_ids_fast(loop_entity_id)?;
633
634        // Pre-allocate with known size
635        let mut polygon_points = Vec::with_capacity(point_ids.len());
636
637        for point_id in point_ids {
638            // Use fast path to extract coordinates directly from raw bytes
639            // Return None if ANY point fails - ensures complete polygon or nothing
640            let (x, y, z) = decoder.get_cartesian_point_fast(point_id)?;
641            polygon_points.push(Point3::new(x, y, z));
642        }
643
644        if polygon_points.len() >= 3 {
645            Some(polygon_points)
646        } else {
647            None
648        }
649    }
650
651    /// Triangulate a single face (can be called in parallel)
652    /// Optimized with fast paths for simple faces
653    #[inline]
654    fn triangulate_face(face: &FaceData) -> FaceResult {
655        let n = face.outer_points.len();
656
657        // FAST PATH: Triangle without holes - no triangulation needed
658        if n == 3 && face.hole_points.is_empty() {
659            let mut positions = Vec::with_capacity(9);
660            for point in &face.outer_points {
661                positions.push(point.x as f32);
662                positions.push(point.y as f32);
663                positions.push(point.z as f32);
664            }
665            return FaceResult {
666                positions,
667                indices: vec![0, 1, 2],
668            };
669        }
670
671        // FAST PATH: Quad without holes - simple fan
672        if n == 4 && face.hole_points.is_empty() {
673            let mut positions = Vec::with_capacity(12);
674            for point in &face.outer_points {
675                positions.push(point.x as f32);
676                positions.push(point.y as f32);
677                positions.push(point.z as f32);
678            }
679            return FaceResult {
680                positions,
681                indices: vec![0, 1, 2, 0, 2, 3],
682            };
683        }
684
685        // FAST PATH: Simple convex polygon without holes
686        if face.hole_points.is_empty() && n <= 8 {
687            // Check if convex by testing cross products in 3D
688            let mut is_convex = true;
689            if n > 4 {
690                use crate::triangulation::calculate_polygon_normal;
691                let normal = calculate_polygon_normal(&face.outer_points);
692                let mut sign = 0i8;
693
694                for i in 0..n {
695                    let p0 = &face.outer_points[i];
696                    let p1 = &face.outer_points[(i + 1) % n];
697                    let p2 = &face.outer_points[(i + 2) % n];
698
699                    let v1 = p1 - p0;
700                    let v2 = p2 - p1;
701                    let cross = v1.cross(&v2);
702                    let dot = cross.dot(&normal);
703
704                    if dot.abs() > 1e-10 {
705                        let current_sign = if dot > 0.0 { 1i8 } else { -1i8 };
706                        if sign == 0 {
707                            sign = current_sign;
708                        } else if sign != current_sign {
709                            is_convex = false;
710                            break;
711                        }
712                    }
713                }
714            }
715
716            if is_convex {
717                let mut positions = Vec::with_capacity(n * 3);
718                for point in &face.outer_points {
719                    positions.push(point.x as f32);
720                    positions.push(point.y as f32);
721                    positions.push(point.z as f32);
722                }
723                let mut indices = Vec::with_capacity((n - 2) * 3);
724                for i in 1..n - 1 {
725                    indices.push(0);
726                    indices.push(i as u32);
727                    indices.push(i as u32 + 1);
728                }
729                return FaceResult { positions, indices };
730            }
731        }
732
733        // SLOW PATH: Complex polygon or polygon with holes
734        use crate::triangulation::{
735            calculate_polygon_normal, project_to_2d, project_to_2d_with_basis,
736            triangulate_polygon_with_holes,
737        };
738
739        let mut positions = Vec::new();
740        let mut indices = Vec::new();
741
742        // Calculate face normal from outer boundary
743        let normal = calculate_polygon_normal(&face.outer_points);
744
745        // Project outer boundary to 2D and get the coordinate system
746        let (outer_2d, u_axis, v_axis, origin) = project_to_2d(&face.outer_points, &normal);
747
748        // Project holes to 2D using the SAME coordinate system as the outer boundary
749        let holes_2d: Vec<Vec<nalgebra::Point2<f64>>> = face
750            .hole_points
751            .iter()
752            .map(|hole| project_to_2d_with_basis(hole, &u_axis, &v_axis, &origin))
753            .collect();
754
755        // Triangulate with holes
756        let tri_indices = match triangulate_polygon_with_holes(&outer_2d, &holes_2d) {
757            Ok(idx) => idx,
758            Err(_) => {
759                // Fallback to simple fan triangulation without holes
760                for point in &face.outer_points {
761                    positions.push(point.x as f32);
762                    positions.push(point.y as f32);
763                    positions.push(point.z as f32);
764                }
765                for i in 1..face.outer_points.len() - 1 {
766                    indices.push(0);
767                    indices.push(i as u32);
768                    indices.push(i as u32 + 1);
769                }
770                return FaceResult { positions, indices };
771            }
772        };
773
774        // Combine all 3D points (outer + holes) in the same order as 2D
775        let mut all_points_3d: Vec<&Point3<f64>> = face.outer_points.iter().collect();
776        for hole in &face.hole_points {
777            all_points_3d.extend(hole.iter());
778        }
779
780        // Add vertices
781        for point in &all_points_3d {
782            positions.push(point.x as f32);
783            positions.push(point.y as f32);
784            positions.push(point.z as f32);
785        }
786
787        // Add triangle indices
788        for i in (0..tri_indices.len()).step_by(3) {
789            indices.push(tri_indices[i] as u32);
790            indices.push(tri_indices[i + 1] as u32);
791            indices.push(tri_indices[i + 2] as u32);
792        }
793
794        FaceResult { positions, indices }
795    }
796
797    /// Batch process multiple FacetedBrep entities for maximum parallelism
798    /// Extracts all face data sequentially, then triangulates ALL faces in one parallel batch
799    /// Returns Vec of (brep_index, Mesh) pairs
800    pub fn process_batch(
801        &self,
802        brep_ids: &[u32],
803        decoder: &mut EntityDecoder,
804    ) -> Vec<(usize, Mesh)> {
805        use rayon::prelude::*;
806
807        // PHASE 1: Sequential - Extract all face data from all BREPs
808        // Each entry: (brep_index, face_data)
809        let mut all_faces: Vec<(usize, FaceData)> = Vec::with_capacity(brep_ids.len() * 10);
810
811        for (brep_idx, &brep_id) in brep_ids.iter().enumerate() {
812            // Decode the BREP entity
813            let brep_entity = match decoder.decode_by_id(brep_id) {
814                Ok(e) => e,
815                Err(_) => continue,
816            };
817
818            // Get closed shell ID
819            let shell_id = match brep_entity.get(0).and_then(|a| a.as_entity_ref()) {
820                Some(id) => id,
821                None => continue,
822            };
823
824            // Get face IDs from shell
825            let face_ids = match decoder.get_entity_ref_list_fast(shell_id) {
826                Some(ids) => ids,
827                None => continue,
828            };
829
830            // Extract face data for each face
831            for face_id in face_ids {
832                let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
833                    Some(ids) => ids,
834                    None => continue,
835                };
836
837                let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
838                let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
839
840                for bound_id in bound_ids {
841                    let bound = match decoder.decode_by_id(bound_id) {
842                        Ok(b) => b,
843                        Err(_) => continue,
844                    };
845
846                    let loop_attr = match bound.get(0) {
847                        Some(attr) => attr,
848                        None => continue,
849                    };
850
851                    let orientation = bound
852                        .get(1)
853                        .map(|v| match v {
854                            // Parser strips dots, so enum value is "T" or "F", not ".T." or ".F."
855                            ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
856                            _ => true,
857                        })
858                        .unwrap_or(true);
859
860                    let mut points = if let Some(loop_id) = loop_attr.as_entity_ref() {
861                        match self.extract_loop_points_fast(loop_id, decoder) {
862                            Some(p) => p,
863                            None => continue,
864                        }
865                    } else {
866                        continue;
867                    };
868
869                    if !orientation {
870                        points.reverse();
871                    }
872
873                    let is_outer = match bound.ifc_type {
874                        IfcType::IfcFaceOuterBound => true,
875                        IfcType::IfcFaceBound => false,
876                        _ => bound.ifc_type.as_str().contains("OUTER"),
877                    };
878
879                    if is_outer || outer_bound_points.is_none() {
880                        if outer_bound_points.is_some() && is_outer {
881                            if let Some(prev_outer) = outer_bound_points.take() {
882                                hole_points.push(prev_outer);
883                            }
884                        }
885                        outer_bound_points = Some(points);
886                    } else {
887                        hole_points.push(points);
888                    }
889                }
890
891                if let Some(outer_points) = outer_bound_points {
892                    all_faces.push((
893                        brep_idx,
894                        FaceData {
895                            outer_points,
896                            hole_points,
897                        },
898                    ));
899                }
900            }
901        }
902
903        // PHASE 2: Parallel - Triangulate ALL faces from ALL BREPs in one batch
904        let face_results: Vec<(usize, FaceResult)> = all_faces
905            .par_iter()
906            .map(|(brep_idx, face)| (*brep_idx, Self::triangulate_face(face)))
907            .collect();
908
909        // PHASE 3: Group results back by BREP index
910        // First, count faces per BREP to pre-allocate
911        let mut face_counts = vec![0usize; brep_ids.len()];
912        for (brep_idx, _) in &face_results {
913            face_counts[*brep_idx] += 1;
914        }
915
916        // Initialize mesh builders for each BREP
917        let mut mesh_builders: Vec<(Vec<f32>, Vec<u32>)> = face_counts
918            .iter()
919            .map(|&count| {
920                (
921                    Vec::with_capacity(count * 100),
922                    Vec::with_capacity(count * 50),
923                )
924            })
925            .collect();
926
927        // Merge face results into their respective meshes
928        for (brep_idx, result) in face_results {
929            let (positions, indices) = &mut mesh_builders[brep_idx];
930            let base_idx = (positions.len() / 3) as u32;
931            positions.extend(result.positions);
932            for idx in result.indices {
933                indices.push(base_idx + idx);
934            }
935        }
936
937        // Convert to final meshes
938        mesh_builders
939            .into_iter()
940            .enumerate()
941            .filter(|(_, (positions, _))| !positions.is_empty())
942            .map(|(brep_idx, (positions, indices))| {
943                (
944                    brep_idx,
945                    Mesh {
946                        positions,
947                        normals: Vec::new(),
948                        indices,
949                    },
950                )
951            })
952            .collect()
953    }
954}
955
956impl GeometryProcessor for FacetedBrepProcessor {
957    fn process(
958        &self,
959        entity: &DecodedEntity,
960        decoder: &mut EntityDecoder,
961        _schema: &IfcSchema,
962    ) -> Result<Mesh> {
963        use rayon::prelude::*;
964
965        // IfcFacetedBrep attributes:
966        // 0: Outer (IfcClosedShell)
967
968        // Get closed shell ID
969        let shell_attr = entity
970            .get(0)
971            .ok_or_else(|| Error::geometry("FacetedBrep missing Outer shell".to_string()))?;
972
973        let shell_id = shell_attr
974            .as_entity_ref()
975            .ok_or_else(|| Error::geometry("Expected entity ref for Outer shell".to_string()))?;
976
977        // FAST PATH: Get face IDs directly from ClosedShell raw bytes
978        let face_ids = decoder
979            .get_entity_ref_list_fast(shell_id)
980            .ok_or_else(|| Error::geometry("Failed to get faces from ClosedShell".to_string()))?;
981
982        // PHASE 1: Sequential - Extract all face data from IFC entities
983        let mut face_data_list: Vec<FaceData> = Vec::with_capacity(face_ids.len());
984
985        for face_id in face_ids {
986            // FAST PATH: Get bound IDs directly from Face raw bytes
987            let bound_ids = match decoder.get_entity_ref_list_fast(face_id) {
988                Some(ids) => ids,
989                None => continue,
990            };
991
992            // Separate outer bound from inner bounds (holes)
993            let mut outer_bound_points: Option<Vec<Point3<f64>>> = None;
994            let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
995
996            for bound_id in bound_ids {
997                // Get bound entity to check type and get loop ref (uses cache)
998                let bound = match decoder.decode_by_id(bound_id) {
999                    Ok(b) => b,
1000                    Err(_) => continue,
1001                };
1002
1003                let loop_attr = match bound.get(0) {
1004                    Some(attr) => attr,
1005                    None => continue,
1006                };
1007
1008                // Get orientation
1009                let orientation = bound
1010                    .get(1)
1011                    .map(|v| match v {
1012                        // Parser strips dots, so enum value is "T" or "F", not ".T." or ".F."
1013                        ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
1014                        _ => true,
1015                    })
1016                    .unwrap_or(true);
1017
1018                // FAST PATH: Get loop points directly from entity ID
1019                let mut points = if let Some(loop_id) = loop_attr.as_entity_ref() {
1020                    match self.extract_loop_points_fast(loop_id, decoder) {
1021                        Some(p) => p,
1022                        None => continue,
1023                    }
1024                } else {
1025                    continue;
1026                };
1027
1028                if !orientation {
1029                    points.reverse();
1030                }
1031
1032                let is_outer = match bound.ifc_type {
1033                    IfcType::IfcFaceOuterBound => true,
1034                    IfcType::IfcFaceBound => false,
1035                    _ => bound.ifc_type.as_str().contains("OUTER"),
1036                };
1037
1038                if is_outer || outer_bound_points.is_none() {
1039                    if outer_bound_points.is_some() && is_outer {
1040                        if let Some(prev_outer) = outer_bound_points.take() {
1041                            hole_points.push(prev_outer);
1042                        }
1043                    }
1044                    outer_bound_points = Some(points);
1045                } else {
1046                    hole_points.push(points);
1047                }
1048            }
1049
1050            if let Some(outer_points) = outer_bound_points {
1051                face_data_list.push(FaceData {
1052                    outer_points,
1053                    hole_points,
1054                });
1055            }
1056        }
1057
1058        // PHASE 2: Parallel - Triangulate all faces concurrently
1059        // Always use parallel for faces (rayon handles small workloads efficiently)
1060        let face_results: Vec<FaceResult> = face_data_list
1061            .par_iter()
1062            .map(Self::triangulate_face)
1063            .collect();
1064
1065        // PHASE 3: Sequential - Merge all face results into final mesh
1066        // Pre-calculate total sizes for efficient allocation
1067        let total_positions: usize = face_results.iter().map(|r| r.positions.len()).sum();
1068        let total_indices: usize = face_results.iter().map(|r| r.indices.len()).sum();
1069
1070        let mut positions = Vec::with_capacity(total_positions);
1071        let mut indices = Vec::with_capacity(total_indices);
1072
1073        for result in face_results {
1074            let base_idx = (positions.len() / 3) as u32;
1075            positions.extend(result.positions);
1076
1077            // Offset indices by base
1078            for idx in result.indices {
1079                indices.push(base_idx + idx);
1080            }
1081        }
1082
1083        Ok(Mesh {
1084            positions,
1085            normals: Vec::new(),
1086            indices,
1087        })
1088    }
1089
1090    fn supported_types(&self) -> Vec<IfcType> {
1091        vec![IfcType::IfcFacetedBrep]
1092    }
1093}
1094
1095impl Default for FacetedBrepProcessor {
1096    fn default() -> Self {
1097        Self::new()
1098    }
1099}
1100
1101/// BooleanResult processor
1102/// Handles IfcBooleanResult and IfcBooleanClippingResult - CSG operations
1103/// Supports half-space clipping for DIFFERENCE operations
1104pub struct BooleanClippingProcessor {
1105    schema: IfcSchema,
1106}
1107
1108impl BooleanClippingProcessor {
1109    pub fn new() -> Self {
1110        Self {
1111            schema: IfcSchema::new(),
1112        }
1113    }
1114
1115    /// Process a solid operand recursively
1116    fn process_operand(
1117        &self,
1118        operand: &DecodedEntity,
1119        decoder: &mut EntityDecoder,
1120    ) -> Result<Mesh> {
1121        match operand.ifc_type {
1122            IfcType::IfcExtrudedAreaSolid => {
1123                let processor = ExtrudedAreaSolidProcessor::new(self.schema.clone());
1124                processor.process(operand, decoder, &self.schema)
1125            }
1126            IfcType::IfcFacetedBrep => {
1127                let processor = FacetedBrepProcessor::new();
1128                processor.process(operand, decoder, &self.schema)
1129            }
1130            IfcType::IfcTriangulatedFaceSet => {
1131                let processor = TriangulatedFaceSetProcessor::new();
1132                processor.process(operand, decoder, &self.schema)
1133            }
1134            IfcType::IfcSweptDiskSolid => {
1135                let processor = SweptDiskSolidProcessor::new(self.schema.clone());
1136                processor.process(operand, decoder, &self.schema)
1137            }
1138            IfcType::IfcRevolvedAreaSolid => {
1139                let processor = RevolvedAreaSolidProcessor::new(self.schema.clone());
1140                processor.process(operand, decoder, &self.schema)
1141            }
1142            IfcType::IfcBooleanResult | IfcType::IfcBooleanClippingResult => {
1143                // Recursive case
1144                self.process(operand, decoder, &self.schema)
1145            }
1146            _ => Ok(Mesh::new()),
1147        }
1148    }
1149
1150    /// Parse IfcHalfSpaceSolid to get clipping plane
1151    /// Returns (plane_point, plane_normal, agreement_flag)
1152    fn parse_half_space_solid(
1153        &self,
1154        half_space: &DecodedEntity,
1155        decoder: &mut EntityDecoder,
1156    ) -> Result<(Point3<f64>, Vector3<f64>, bool)> {
1157        // IfcHalfSpaceSolid attributes:
1158        // 0: BaseSurface (IfcSurface - usually IfcPlane)
1159        // 1: AgreementFlag (boolean - true means material is on positive side)
1160
1161        let surface_attr = half_space
1162            .get(0)
1163            .ok_or_else(|| Error::geometry("HalfSpaceSolid missing BaseSurface".to_string()))?;
1164
1165        let surface = decoder
1166            .resolve_ref(surface_attr)?
1167            .ok_or_else(|| Error::geometry("Failed to resolve BaseSurface".to_string()))?;
1168
1169        // Get agreement flag - defaults to true
1170        let agreement = half_space
1171            .get(1)
1172            .map(|v| match v {
1173                // Parser strips dots, so enum value is "T" or "F", not ".T." or ".F."
1174                ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
1175                _ => true,
1176            })
1177            .unwrap_or(true);
1178
1179        // Parse IfcPlane
1180        if surface.ifc_type != IfcType::IfcPlane {
1181            return Err(Error::geometry(format!(
1182                "Expected IfcPlane for HalfSpaceSolid, got {}",
1183                surface.ifc_type
1184            )));
1185        }
1186
1187        // IfcPlane has one attribute: Position (IfcAxis2Placement3D)
1188        let position_attr = surface
1189            .get(0)
1190            .ok_or_else(|| Error::geometry("IfcPlane missing Position".to_string()))?;
1191
1192        let position = decoder
1193            .resolve_ref(position_attr)?
1194            .ok_or_else(|| Error::geometry("Failed to resolve Plane position".to_string()))?;
1195
1196        // Parse IfcAxis2Placement3D
1197        // Location (Point), Axis (Direction - Z), RefDirection (Direction - X)
1198        let location = {
1199            let loc_attr = position
1200                .get(0)
1201                .ok_or_else(|| Error::geometry("Axis2Placement3D missing Location".to_string()))?;
1202            let loc = decoder
1203                .resolve_ref(loc_attr)?
1204                .ok_or_else(|| Error::geometry("Failed to resolve plane location".to_string()))?;
1205            let coords = loc
1206                .get(0)
1207                .and_then(|v| v.as_list())
1208                .ok_or_else(|| Error::geometry("Location missing coordinates".to_string()))?;
1209            Point3::new(
1210                coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
1211                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
1212                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
1213            )
1214        };
1215
1216        let normal = {
1217            if let Some(axis_attr) = position.get(1) {
1218                if !axis_attr.is_null() {
1219                    let axis = decoder.resolve_ref(axis_attr)?.ok_or_else(|| {
1220                        Error::geometry("Failed to resolve plane axis".to_string())
1221                    })?;
1222                    let coords = axis.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
1223                        Error::geometry("Axis missing direction ratios".to_string())
1224                    })?;
1225                    Vector3::new(
1226                        coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
1227                        coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
1228                        coords.get(2).and_then(|v| v.as_float()).unwrap_or(1.0),
1229                    )
1230                    .normalize()
1231                } else {
1232                    Vector3::new(0.0, 0.0, 1.0)
1233                }
1234            } else {
1235                Vector3::new(0.0, 0.0, 1.0)
1236            }
1237        };
1238
1239        Ok((location, normal, agreement))
1240    }
1241
1242    /// Apply half-space clipping to mesh
1243    fn clip_mesh_with_half_space(
1244        &self,
1245        mesh: &Mesh,
1246        plane_point: Point3<f64>,
1247        plane_normal: Vector3<f64>,
1248        agreement: bool,
1249    ) -> Result<Mesh> {
1250        use crate::csg::{ClippingProcessor, Plane};
1251
1252        // For DIFFERENCE operation with HalfSpaceSolid:
1253        // - AgreementFlag=.T. means keep material on positive side of plane
1254        // - AgreementFlag=.F. means keep material on negative side of plane
1255        // But we're SUBTRACTING the half-space, so we invert
1256        let clip_normal = if agreement {
1257            -plane_normal // Subtract positive side = keep negative side
1258        } else {
1259            plane_normal // Subtract negative side = keep positive side
1260        };
1261
1262        let plane = Plane::new(plane_point, clip_normal);
1263        let processor = ClippingProcessor::new();
1264        processor.clip_mesh(mesh, &plane)
1265    }
1266}
1267
1268impl GeometryProcessor for BooleanClippingProcessor {
1269    fn process(
1270        &self,
1271        entity: &DecodedEntity,
1272        decoder: &mut EntityDecoder,
1273        _schema: &IfcSchema,
1274    ) -> Result<Mesh> {
1275        // IfcBooleanResult attributes:
1276        // 0: Operator (.DIFFERENCE., .UNION., .INTERSECTION.)
1277        // 1: FirstOperand (base geometry)
1278        // 2: SecondOperand (clipping geometry)
1279
1280        // Get operator
1281        let operator = entity
1282            .get(0)
1283            .and_then(|v| match v {
1284                ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
1285                _ => None,
1286            })
1287            .unwrap_or(".DIFFERENCE.");
1288
1289        // Get first operand (base geometry)
1290        let first_operand_attr = entity
1291            .get(1)
1292            .ok_or_else(|| Error::geometry("BooleanResult missing FirstOperand".to_string()))?;
1293
1294        let first_operand = decoder
1295            .resolve_ref(first_operand_attr)?
1296            .ok_or_else(|| Error::geometry("Failed to resolve FirstOperand".to_string()))?;
1297
1298        // Process first operand to get base mesh
1299        let mesh = self.process_operand(&first_operand, decoder)?;
1300
1301        if mesh.is_empty() {
1302            return Ok(mesh);
1303        }
1304
1305        // Get second operand
1306        let second_operand_attr = entity
1307            .get(2)
1308            .ok_or_else(|| Error::geometry("BooleanResult missing SecondOperand".to_string()))?;
1309
1310        let second_operand = decoder
1311            .resolve_ref(second_operand_attr)?
1312            .ok_or_else(|| Error::geometry("Failed to resolve SecondOperand".to_string()))?;
1313
1314        // Handle DIFFERENCE operation
1315        if operator == ".DIFFERENCE." {
1316            // Check if second operand is a half-space solid (can clip efficiently)
1317            if second_operand.ifc_type == IfcType::IfcHalfSpaceSolid
1318                || second_operand.ifc_type == IfcType::IfcPolygonalBoundedHalfSpace
1319            {
1320                let (plane_point, plane_normal, agreement) =
1321                    self.parse_half_space_solid(&second_operand, decoder)?;
1322                return self.clip_mesh_with_half_space(&mesh, plane_point, plane_normal, agreement);
1323            }
1324
1325            // For solid-solid difference, we can't do proper CSG without a full boolean library
1326            // Just return the first operand for now - this gives approximate geometry
1327            #[cfg(debug_assertions)]
1328            eprintln!(
1329                "[WARN] CSG operation {} not fully supported, returning first operand only",
1330                operator
1331            );
1332        }
1333
1334        // For UNION and INTERSECTION, and solid-solid DIFFERENCE,
1335        // just return the first operand for now
1336        #[cfg(debug_assertions)]
1337        if operator != ".DIFFERENCE." {
1338            eprintln!(
1339                "[WARN] CSG operation {} not fully supported, returning first operand only",
1340                operator
1341            );
1342        }
1343        Ok(mesh)
1344    }
1345
1346    fn supported_types(&self) -> Vec<IfcType> {
1347        vec![IfcType::IfcBooleanResult, IfcType::IfcBooleanClippingResult]
1348    }
1349}
1350
1351impl Default for BooleanClippingProcessor {
1352    fn default() -> Self {
1353        Self::new()
1354    }
1355}
1356
1357/// MappedItem processor (P0)
1358/// Handles IfcMappedItem - geometry instancing
1359pub struct MappedItemProcessor;
1360
1361impl MappedItemProcessor {
1362    pub fn new() -> Self {
1363        Self
1364    }
1365}
1366
1367impl GeometryProcessor for MappedItemProcessor {
1368    fn process(
1369        &self,
1370        entity: &DecodedEntity,
1371        decoder: &mut EntityDecoder,
1372        schema: &IfcSchema,
1373    ) -> Result<Mesh> {
1374        // IfcMappedItem attributes:
1375        // 0: MappingSource (IfcRepresentationMap)
1376        // 1: MappingTarget (IfcCartesianTransformationOperator)
1377
1378        // Get mapping source
1379        let source_attr = entity
1380            .get(0)
1381            .ok_or_else(|| Error::geometry("MappedItem missing MappingSource".to_string()))?;
1382
1383        let source_entity = decoder
1384            .resolve_ref(source_attr)?
1385            .ok_or_else(|| Error::geometry("Failed to resolve MappingSource".to_string()))?;
1386
1387        // IfcRepresentationMap has:
1388        // 0: MappingOrigin (IfcAxis2Placement)
1389        // 1: MappedRepresentation (IfcRepresentation)
1390
1391        let mapped_rep_attr = source_entity.get(1).ok_or_else(|| {
1392            Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
1393        })?;
1394
1395        let mapped_rep = decoder
1396            .resolve_ref(mapped_rep_attr)?
1397            .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
1398
1399        // Get representation items
1400        let items_attr = mapped_rep
1401            .get(3)
1402            .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
1403
1404        let items = decoder.resolve_ref_list(items_attr)?;
1405
1406        // Process all items and merge
1407        let mut mesh = Mesh::new();
1408        for item in items {
1409            let item_mesh = match item.ifc_type {
1410                IfcType::IfcExtrudedAreaSolid => {
1411                    let processor = ExtrudedAreaSolidProcessor::new(schema.clone());
1412                    processor.process(&item, decoder, schema)?
1413                }
1414                IfcType::IfcTriangulatedFaceSet => {
1415                    let processor = TriangulatedFaceSetProcessor::new();
1416                    processor.process(&item, decoder, schema)?
1417                }
1418                IfcType::IfcFacetedBrep => {
1419                    let processor = FacetedBrepProcessor::new();
1420                    processor.process(&item, decoder, schema)?
1421                }
1422                IfcType::IfcSweptDiskSolid => {
1423                    let processor = SweptDiskSolidProcessor::new(schema.clone());
1424                    processor.process(&item, decoder, schema)?
1425                }
1426                IfcType::IfcBooleanClippingResult | IfcType::IfcBooleanResult => {
1427                    let processor = BooleanClippingProcessor::new();
1428                    processor.process(&item, decoder, schema)?
1429                }
1430                IfcType::IfcRevolvedAreaSolid => {
1431                    let processor = RevolvedAreaSolidProcessor::new(schema.clone());
1432                    processor.process(&item, decoder, schema)?
1433                }
1434                _ => continue, // Skip unsupported types
1435            };
1436            mesh.merge(&item_mesh);
1437        }
1438
1439        // Note: MappingTarget transformation is applied by the router's process_mapped_item_cached
1440        // when MappedItem is encountered through process_representation_item. This processor
1441        // is a fallback that doesn't have access to the router's transformation logic.
1442
1443        Ok(mesh)
1444    }
1445
1446    fn supported_types(&self) -> Vec<IfcType> {
1447        vec![IfcType::IfcMappedItem]
1448    }
1449}
1450
1451impl Default for MappedItemProcessor {
1452    fn default() -> Self {
1453        Self::new()
1454    }
1455}
1456
1457/// SweptDiskSolid processor
1458/// Handles IfcSweptDiskSolid - sweeps a circular profile along a curve
1459pub struct SweptDiskSolidProcessor {
1460    profile_processor: ProfileProcessor,
1461}
1462
1463impl SweptDiskSolidProcessor {
1464    pub fn new(schema: IfcSchema) -> Self {
1465        Self {
1466            profile_processor: ProfileProcessor::new(schema),
1467        }
1468    }
1469}
1470
1471impl GeometryProcessor for SweptDiskSolidProcessor {
1472    fn process(
1473        &self,
1474        entity: &DecodedEntity,
1475        decoder: &mut EntityDecoder,
1476        _schema: &IfcSchema,
1477    ) -> Result<Mesh> {
1478        // IfcSweptDiskSolid attributes:
1479        // 0: Directrix (IfcCurve) - the path to sweep along
1480        // 1: Radius (IfcPositiveLengthMeasure) - outer radius
1481        // 2: InnerRadius (optional) - inner radius for hollow tubes
1482        // 3: StartParam (optional)
1483        // 4: EndParam (optional)
1484
1485        let directrix_attr = entity
1486            .get(0)
1487            .ok_or_else(|| Error::geometry("SweptDiskSolid missing Directrix".to_string()))?;
1488
1489        let radius = entity
1490            .get_float(1)
1491            .ok_or_else(|| Error::geometry("SweptDiskSolid missing Radius".to_string()))?;
1492
1493        // Get inner radius if hollow
1494        let _inner_radius = entity.get_float(2);
1495
1496        // Resolve the directrix curve
1497        let directrix = decoder
1498            .resolve_ref(directrix_attr)?
1499            .ok_or_else(|| Error::geometry("Failed to resolve Directrix".to_string()))?;
1500
1501        // Get points along the curve
1502        let curve_points = self
1503            .profile_processor
1504            .get_curve_points(&directrix, decoder)?;
1505
1506        if curve_points.len() < 2 {
1507            return Ok(Mesh::new()); // Not enough points
1508        }
1509
1510        // Generate tube mesh by sweeping circle along curve
1511        let segments = 12; // Number of segments around the circle
1512        let mut positions = Vec::new();
1513        let mut indices = Vec::new();
1514
1515        // For each point on the curve, create a ring of vertices
1516        for i in 0..curve_points.len() {
1517            let p = curve_points[i];
1518
1519            // Calculate tangent direction
1520            let tangent = if i == 0 {
1521                (curve_points[1] - curve_points[0]).normalize()
1522            } else if i == curve_points.len() - 1 {
1523                (curve_points[i] - curve_points[i - 1]).normalize()
1524            } else {
1525                ((curve_points[i + 1] - curve_points[i - 1]) / 2.0).normalize()
1526            };
1527
1528            // Create perpendicular vectors using cross product
1529            // First, find a vector not parallel to tangent
1530            let up = if tangent.x.abs() < 0.9 {
1531                Vector3::new(1.0, 0.0, 0.0)
1532            } else {
1533                Vector3::new(0.0, 1.0, 0.0)
1534            };
1535
1536            let perp1 = tangent.cross(&up).normalize();
1537            let perp2 = tangent.cross(&perp1).normalize();
1538
1539            // Create ring of vertices
1540            for j in 0..segments {
1541                let angle = 2.0 * std::f64::consts::PI * j as f64 / segments as f64;
1542                let offset = perp1 * (radius * angle.cos()) + perp2 * (radius * angle.sin());
1543                let vertex = p + offset;
1544
1545                positions.push(vertex.x as f32);
1546                positions.push(vertex.y as f32);
1547                positions.push(vertex.z as f32);
1548            }
1549
1550            // Create triangles connecting this ring to the next
1551            if i < curve_points.len() - 1 {
1552                let base = (i * segments) as u32;
1553                let next_base = ((i + 1) * segments) as u32;
1554
1555                for j in 0..segments {
1556                    let j_next = (j + 1) % segments;
1557
1558                    // Two triangles per quad
1559                    indices.push(base + j as u32);
1560                    indices.push(next_base + j as u32);
1561                    indices.push(next_base + j_next as u32);
1562
1563                    indices.push(base + j as u32);
1564                    indices.push(next_base + j_next as u32);
1565                    indices.push(base + j_next as u32);
1566                }
1567            }
1568        }
1569
1570        // Add end caps
1571        // Start cap
1572        let center_idx = (positions.len() / 3) as u32;
1573        let start = curve_points[0];
1574        positions.push(start.x as f32);
1575        positions.push(start.y as f32);
1576        positions.push(start.z as f32);
1577
1578        for j in 0..segments {
1579            let j_next = (j + 1) % segments;
1580            indices.push(center_idx);
1581            indices.push(j_next as u32);
1582            indices.push(j as u32);
1583        }
1584
1585        // End cap
1586        let end_center_idx = (positions.len() / 3) as u32;
1587        let end_base = ((curve_points.len() - 1) * segments) as u32;
1588        let end = curve_points[curve_points.len() - 1];
1589        positions.push(end.x as f32);
1590        positions.push(end.y as f32);
1591        positions.push(end.z as f32);
1592
1593        for j in 0..segments {
1594            let j_next = (j + 1) % segments;
1595            indices.push(end_center_idx);
1596            indices.push(end_base + j as u32);
1597            indices.push(end_base + j_next as u32);
1598        }
1599
1600        Ok(Mesh {
1601            positions,
1602            normals: Vec::new(),
1603            indices,
1604        })
1605    }
1606
1607    fn supported_types(&self) -> Vec<IfcType> {
1608        vec![IfcType::IfcSweptDiskSolid]
1609    }
1610}
1611
1612impl Default for SweptDiskSolidProcessor {
1613    fn default() -> Self {
1614        Self::new(IfcSchema::new())
1615    }
1616}
1617
1618/// RevolvedAreaSolid processor
1619/// Handles IfcRevolvedAreaSolid - rotates a 2D profile around an axis
1620pub struct RevolvedAreaSolidProcessor {
1621    profile_processor: ProfileProcessor,
1622}
1623
1624impl RevolvedAreaSolidProcessor {
1625    pub fn new(schema: IfcSchema) -> Self {
1626        Self {
1627            profile_processor: ProfileProcessor::new(schema),
1628        }
1629    }
1630}
1631
1632impl GeometryProcessor for RevolvedAreaSolidProcessor {
1633    fn process(
1634        &self,
1635        entity: &DecodedEntity,
1636        decoder: &mut EntityDecoder,
1637        _schema: &IfcSchema,
1638    ) -> Result<Mesh> {
1639        // IfcRevolvedAreaSolid attributes:
1640        // 0: SweptArea (IfcProfileDef) - the 2D profile to revolve
1641        // 1: Position (IfcAxis2Placement3D) - placement of the solid
1642        // 2: Axis (IfcAxis1Placement) - the axis of revolution
1643        // 3: Angle (IfcPlaneAngleMeasure) - revolution angle in radians
1644
1645        let profile_attr = entity
1646            .get(0)
1647            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing SweptArea".to_string()))?;
1648
1649        let profile = decoder
1650            .resolve_ref(profile_attr)?
1651            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
1652
1653        // Get axis placement (attribute 2)
1654        let axis_attr = entity
1655            .get(2)
1656            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Axis".to_string()))?;
1657
1658        let axis_placement = decoder
1659            .resolve_ref(axis_attr)?
1660            .ok_or_else(|| Error::geometry("Failed to resolve Axis".to_string()))?;
1661
1662        // Get angle (attribute 3)
1663        let angle = entity
1664            .get_float(3)
1665            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Angle".to_string()))?;
1666
1667        // Get the 2D profile points
1668        let profile_2d = self.profile_processor.process(&profile, decoder)?;
1669        if profile_2d.outer.is_empty() {
1670            return Ok(Mesh::new());
1671        }
1672
1673        // Parse axis placement to get axis point and direction
1674        // IfcAxis1Placement: Location, Axis (optional)
1675        let axis_location = {
1676            let loc_attr = axis_placement
1677                .get(0)
1678                .ok_or_else(|| Error::geometry("Axis1Placement missing Location".to_string()))?;
1679            let loc = decoder
1680                .resolve_ref(loc_attr)?
1681                .ok_or_else(|| Error::geometry("Failed to resolve axis location".to_string()))?;
1682            let coords = loc
1683                .get(0)
1684                .and_then(|v| v.as_list())
1685                .ok_or_else(|| Error::geometry("Axis location missing coordinates".to_string()))?;
1686            Point3::new(
1687                coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
1688                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
1689                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
1690            )
1691        };
1692
1693        let axis_direction = {
1694            if let Some(dir_attr) = axis_placement.get(1) {
1695                if !dir_attr.is_null() {
1696                    let dir = decoder.resolve_ref(dir_attr)?.ok_or_else(|| {
1697                        Error::geometry("Failed to resolve axis direction".to_string())
1698                    })?;
1699                    let coords = dir.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
1700                        Error::geometry("Axis direction missing coordinates".to_string())
1701                    })?;
1702                    Vector3::new(
1703                        coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
1704                        coords.get(1).and_then(|v| v.as_float()).unwrap_or(1.0),
1705                        coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
1706                    )
1707                    .normalize()
1708                } else {
1709                    Vector3::new(0.0, 1.0, 0.0) // Default Y axis
1710                }
1711            } else {
1712                Vector3::new(0.0, 1.0, 0.0) // Default Y axis
1713            }
1714        };
1715
1716        // Generate revolved mesh
1717        // Number of segments depends on angle
1718        let full_circle = angle.abs() >= std::f64::consts::PI * 1.99;
1719        let segments = if full_circle {
1720            24 // Full revolution
1721        } else {
1722            ((angle.abs() / std::f64::consts::PI * 12.0).ceil() as usize).max(4)
1723        };
1724
1725        let profile_points = &profile_2d.outer;
1726        let num_profile_points = profile_points.len();
1727
1728        let mut positions = Vec::new();
1729        let mut indices = Vec::new();
1730
1731        // For each segment around the revolution
1732        for i in 0..=segments {
1733            let t = if full_circle && i == segments {
1734                0.0 // Close the loop exactly
1735            } else {
1736                angle * i as f64 / segments as f64
1737            };
1738
1739            // Rotation matrix around axis
1740            let cos_t = t.cos();
1741            let sin_t = t.sin();
1742            let (ax, ay, az) = (axis_direction.x, axis_direction.y, axis_direction.z);
1743
1744            // Rodrigues' rotation formula components
1745            let k_matrix = |v: Vector3<f64>| -> Vector3<f64> {
1746                Vector3::new(
1747                    ay * v.z - az * v.y,
1748                    az * v.x - ax * v.z,
1749                    ax * v.y - ay * v.x,
1750                )
1751            };
1752
1753            // For each point in the profile
1754            for (j, p2d) in profile_points.iter().enumerate() {
1755                // Profile point in 3D (assume profile is in XY plane, rotated around Y axis)
1756                // The 2D profile X becomes distance from axis, Y becomes height along axis
1757                let radius = p2d.x;
1758                let height = p2d.y;
1759
1760                // Initial position before rotation (in the plane containing the axis)
1761                let v = Vector3::new(radius, 0.0, 0.0);
1762
1763                // Rodrigues' rotation: v_rot = v*cos(t) + (k x v)*sin(t) + k*(k.v)*(1-cos(t))
1764                let k_cross_v = k_matrix(v);
1765                let k_dot_v = ax * v.x + ay * v.y + az * v.z;
1766
1767                let v_rot =
1768                    v * cos_t + k_cross_v * sin_t + axis_direction * k_dot_v * (1.0 - cos_t);
1769
1770                // Final position = axis_location + height along axis + rotated radius
1771                let pos = axis_location + axis_direction * height + v_rot;
1772
1773                positions.push(pos.x as f32);
1774                positions.push(pos.y as f32);
1775                positions.push(pos.z as f32);
1776
1777                // Create triangles (except for the last segment if it connects back)
1778                if i < segments && j < num_profile_points - 1 {
1779                    let current = (i * num_profile_points + j) as u32;
1780                    let next_seg = ((i + 1) * num_profile_points + j) as u32;
1781                    let current_next = current + 1;
1782                    let next_seg_next = next_seg + 1;
1783
1784                    // Two triangles per quad
1785                    indices.push(current);
1786                    indices.push(next_seg);
1787                    indices.push(next_seg_next);
1788
1789                    indices.push(current);
1790                    indices.push(next_seg_next);
1791                    indices.push(current_next);
1792                }
1793            }
1794        }
1795
1796        // Add end caps if not a full revolution
1797        if !full_circle {
1798            // Start cap
1799            let start_center_idx = (positions.len() / 3) as u32;
1800            let start_center = axis_location
1801                + axis_direction
1802                    * (profile_points.iter().map(|p| p.y).sum::<f64>()
1803                        / profile_points.len() as f64);
1804            positions.push(start_center.x as f32);
1805            positions.push(start_center.y as f32);
1806            positions.push(start_center.z as f32);
1807
1808            for j in 0..num_profile_points - 1 {
1809                indices.push(start_center_idx);
1810                indices.push(j as u32 + 1);
1811                indices.push(j as u32);
1812            }
1813
1814            // End cap
1815            let end_center_idx = (positions.len() / 3) as u32;
1816            let end_base = (segments * num_profile_points) as u32;
1817            positions.push(start_center.x as f32);
1818            positions.push(start_center.y as f32);
1819            positions.push(start_center.z as f32);
1820
1821            for j in 0..num_profile_points - 1 {
1822                indices.push(end_center_idx);
1823                indices.push(end_base + j as u32);
1824                indices.push(end_base + j as u32 + 1);
1825            }
1826        }
1827
1828        Ok(Mesh {
1829            positions,
1830            normals: Vec::new(),
1831            indices,
1832        })
1833    }
1834
1835    fn supported_types(&self) -> Vec<IfcType> {
1836        vec![IfcType::IfcRevolvedAreaSolid]
1837    }
1838}
1839
1840impl Default for RevolvedAreaSolidProcessor {
1841    fn default() -> Self {
1842        Self::new(IfcSchema::new())
1843    }
1844}
1845
1846/// AdvancedBrep processor
1847/// Handles IfcAdvancedBrep and IfcAdvancedBrepWithVoids - NURBS/B-spline surfaces
1848/// Supports planar faces and B-spline surface tessellation
1849pub struct AdvancedBrepProcessor;
1850
1851impl AdvancedBrepProcessor {
1852    pub fn new() -> Self {
1853        Self
1854    }
1855
1856    /// Evaluate a B-spline basis function (Cox-de Boor recursion)
1857    #[inline]
1858    fn bspline_basis(i: usize, p: usize, u: f64, knots: &[f64]) -> f64 {
1859        if p == 0 {
1860            if knots[i] <= u && u < knots[i + 1] {
1861                1.0
1862            } else {
1863                0.0
1864            }
1865        } else {
1866            let left = {
1867                let denom = knots[i + p] - knots[i];
1868                if denom.abs() < 1e-10 {
1869                    0.0
1870                } else {
1871                    (u - knots[i]) / denom * Self::bspline_basis(i, p - 1, u, knots)
1872                }
1873            };
1874            let right = {
1875                let denom = knots[i + p + 1] - knots[i + 1];
1876                if denom.abs() < 1e-10 {
1877                    0.0
1878                } else {
1879                    (knots[i + p + 1] - u) / denom * Self::bspline_basis(i + 1, p - 1, u, knots)
1880                }
1881            };
1882            left + right
1883        }
1884    }
1885
1886    /// Evaluate a B-spline surface at parameter (u, v)
1887    fn evaluate_bspline_surface(
1888        u: f64,
1889        v: f64,
1890        u_degree: usize,
1891        v_degree: usize,
1892        control_points: &[Vec<Point3<f64>>],
1893        u_knots: &[f64],
1894        v_knots: &[f64],
1895    ) -> Point3<f64> {
1896        let _n_u = control_points.len();
1897
1898        let mut result = Point3::new(0.0, 0.0, 0.0);
1899
1900        for (i, row) in control_points.iter().enumerate() {
1901            let n_i = Self::bspline_basis(i, u_degree, u, u_knots);
1902            for (j, cp) in row.iter().enumerate() {
1903                let n_j = Self::bspline_basis(j, v_degree, v, v_knots);
1904                let weight = n_i * n_j;
1905                if weight.abs() > 1e-10 {
1906                    result.x += weight * cp.x;
1907                    result.y += weight * cp.y;
1908                    result.z += weight * cp.z;
1909                }
1910            }
1911        }
1912
1913        result
1914    }
1915
1916    /// Tessellate a B-spline surface into triangles
1917    fn tessellate_bspline_surface(
1918        u_degree: usize,
1919        v_degree: usize,
1920        control_points: &[Vec<Point3<f64>>],
1921        u_knots: &[f64],
1922        v_knots: &[f64],
1923        u_segments: usize,
1924        v_segments: usize,
1925    ) -> (Vec<f32>, Vec<u32>) {
1926        let mut positions = Vec::new();
1927        let mut indices = Vec::new();
1928
1929        // Get parameter domain
1930        let u_min = u_knots[u_degree];
1931        let u_max = u_knots[u_knots.len() - u_degree - 1];
1932        let v_min = v_knots[v_degree];
1933        let v_max = v_knots[v_knots.len() - v_degree - 1];
1934
1935        // Evaluate surface on a grid
1936        for i in 0..=u_segments {
1937            let u = u_min + (u_max - u_min) * (i as f64 / u_segments as f64);
1938            // Clamp u to slightly inside the domain to avoid edge issues
1939            let u = u.min(u_max - 1e-6).max(u_min);
1940
1941            for j in 0..=v_segments {
1942                let v = v_min + (v_max - v_min) * (j as f64 / v_segments as f64);
1943                let v = v.min(v_max - 1e-6).max(v_min);
1944
1945                let point = Self::evaluate_bspline_surface(
1946                    u,
1947                    v,
1948                    u_degree,
1949                    v_degree,
1950                    control_points,
1951                    u_knots,
1952                    v_knots,
1953                );
1954
1955                positions.push(point.x as f32);
1956                positions.push(point.y as f32);
1957                positions.push(point.z as f32);
1958
1959                // Create triangles
1960                if i < u_segments && j < v_segments {
1961                    let base = (i * (v_segments + 1) + j) as u32;
1962                    let next_u = base + (v_segments + 1) as u32;
1963
1964                    // Two triangles per quad
1965                    indices.push(base);
1966                    indices.push(base + 1);
1967                    indices.push(next_u + 1);
1968
1969                    indices.push(base);
1970                    indices.push(next_u + 1);
1971                    indices.push(next_u);
1972                }
1973            }
1974        }
1975
1976        (positions, indices)
1977    }
1978
1979    /// Parse control points from B-spline surface entity
1980    fn parse_control_points(
1981        &self,
1982        bspline: &DecodedEntity,
1983        decoder: &mut EntityDecoder,
1984    ) -> Result<Vec<Vec<Point3<f64>>>> {
1985        // Attribute 2: ControlPointsList (LIST of LIST of IfcCartesianPoint)
1986        let cp_list_attr = bspline.get(2).ok_or_else(|| {
1987            Error::geometry("BSplineSurface missing ControlPointsList".to_string())
1988        })?;
1989
1990        let rows = cp_list_attr
1991            .as_list()
1992            .ok_or_else(|| Error::geometry("Expected control point list".to_string()))?;
1993
1994        let mut result = Vec::with_capacity(rows.len());
1995
1996        for row in rows {
1997            let cols = row
1998                .as_list()
1999                .ok_or_else(|| Error::geometry("Expected control point row".to_string()))?;
2000
2001            let mut row_points = Vec::with_capacity(cols.len());
2002            for col in cols {
2003                if let Some(point_id) = col.as_entity_ref() {
2004                    let point = decoder.decode_by_id(point_id)?;
2005                    let coords = point.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
2006                        Error::geometry("CartesianPoint missing coordinates".to_string())
2007                    })?;
2008
2009                    let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
2010                    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
2011                    let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
2012
2013                    row_points.push(Point3::new(x, y, z));
2014                }
2015            }
2016            result.push(row_points);
2017        }
2018
2019        Ok(result)
2020    }
2021
2022    /// Expand knot vector based on multiplicities
2023    fn expand_knots(knot_values: &[f64], multiplicities: &[i64]) -> Vec<f64> {
2024        let mut expanded = Vec::new();
2025        for (knot, &mult) in knot_values.iter().zip(multiplicities.iter()) {
2026            for _ in 0..mult {
2027                expanded.push(*knot);
2028            }
2029        }
2030        expanded
2031    }
2032
2033    /// Parse knot vectors from B-spline surface entity
2034    fn parse_knot_vectors(&self, bspline: &DecodedEntity) -> Result<(Vec<f64>, Vec<f64>)> {
2035        // IFCBSPLINESURFACEWITHKNOTS attributes:
2036        // 0: UDegree
2037        // 1: VDegree
2038        // 2: ControlPointsList (already parsed)
2039        // 3: SurfaceForm
2040        // 4: UClosed
2041        // 5: VClosed
2042        // 6: SelfIntersect
2043        // 7: UMultiplicities (LIST of INTEGER)
2044        // 8: VMultiplicities (LIST of INTEGER)
2045        // 9: UKnots (LIST of REAL)
2046        // 10: VKnots (LIST of REAL)
2047        // 11: KnotSpec
2048
2049        // Get U multiplicities
2050        let u_mult_attr = bspline
2051            .get(7)
2052            .ok_or_else(|| Error::geometry("BSplineSurface missing UMultiplicities".to_string()))?;
2053        let u_mults: Vec<i64> = u_mult_attr
2054            .as_list()
2055            .ok_or_else(|| Error::geometry("Expected U multiplicities list".to_string()))?
2056            .iter()
2057            .filter_map(|v| v.as_int())
2058            .collect();
2059
2060        // Get V multiplicities
2061        let v_mult_attr = bspline
2062            .get(8)
2063            .ok_or_else(|| Error::geometry("BSplineSurface missing VMultiplicities".to_string()))?;
2064        let v_mults: Vec<i64> = v_mult_attr
2065            .as_list()
2066            .ok_or_else(|| Error::geometry("Expected V multiplicities list".to_string()))?
2067            .iter()
2068            .filter_map(|v| v.as_int())
2069            .collect();
2070
2071        // Get U knots
2072        let u_knots_attr = bspline
2073            .get(9)
2074            .ok_or_else(|| Error::geometry("BSplineSurface missing UKnots".to_string()))?;
2075        let u_knot_values: Vec<f64> = u_knots_attr
2076            .as_list()
2077            .ok_or_else(|| Error::geometry("Expected U knots list".to_string()))?
2078            .iter()
2079            .filter_map(|v| v.as_float())
2080            .collect();
2081
2082        // Get V knots
2083        let v_knots_attr = bspline
2084            .get(10)
2085            .ok_or_else(|| Error::geometry("BSplineSurface missing VKnots".to_string()))?;
2086        let v_knot_values: Vec<f64> = v_knots_attr
2087            .as_list()
2088            .ok_or_else(|| Error::geometry("Expected V knots list".to_string()))?
2089            .iter()
2090            .filter_map(|v| v.as_float())
2091            .collect();
2092
2093        // Expand knot vectors with multiplicities
2094        let u_knots = Self::expand_knots(&u_knot_values, &u_mults);
2095        let v_knots = Self::expand_knots(&v_knot_values, &v_mults);
2096
2097        Ok((u_knots, v_knots))
2098    }
2099
2100    /// Process a planar face (IfcPlane surface)
2101    fn process_planar_face(
2102        &self,
2103        face: &DecodedEntity,
2104        decoder: &mut EntityDecoder,
2105    ) -> Result<(Vec<f32>, Vec<u32>)> {
2106        // Get bounds from face (attribute 0)
2107        let bounds_attr = face
2108            .get(0)
2109            .ok_or_else(|| Error::geometry("AdvancedFace missing Bounds".to_string()))?;
2110
2111        let bounds = bounds_attr
2112            .as_list()
2113            .ok_or_else(|| Error::geometry("Expected bounds list".to_string()))?;
2114
2115        let mut positions = Vec::new();
2116        let mut indices = Vec::new();
2117
2118        for bound in bounds {
2119            if let Some(bound_id) = bound.as_entity_ref() {
2120                let bound_entity = decoder.decode_by_id(bound_id)?;
2121
2122                // Get the loop (attribute 0: Bound)
2123                let loop_attr = bound_entity
2124                    .get(0)
2125                    .ok_or_else(|| Error::geometry("FaceBound missing Bound".to_string()))?;
2126
2127                let loop_entity = decoder
2128                    .resolve_ref(loop_attr)?
2129                    .ok_or_else(|| Error::geometry("Failed to resolve loop".to_string()))?;
2130
2131                // Get oriented edges from edge loop
2132                if loop_entity
2133                    .ifc_type
2134                    .as_str()
2135                    .eq_ignore_ascii_case("IFCEDGELOOP")
2136                {
2137                    let edges_attr = loop_entity
2138                        .get(0)
2139                        .ok_or_else(|| Error::geometry("EdgeLoop missing EdgeList".to_string()))?;
2140
2141                    let edges = edges_attr
2142                        .as_list()
2143                        .ok_or_else(|| Error::geometry("Expected edge list".to_string()))?;
2144
2145                    let mut polygon_points = Vec::new();
2146
2147                    for edge_ref in edges {
2148                        if let Some(edge_id) = edge_ref.as_entity_ref() {
2149                            let oriented_edge = decoder.decode_by_id(edge_id)?;
2150
2151                            // IfcOrientedEdge: EdgeStart, EdgeEnd, EdgeElement, Orientation
2152                            // We need EdgeStart vertex
2153                            let start_attr = oriented_edge.get(0).ok_or_else(|| {
2154                                Error::geometry("OrientedEdge missing EdgeStart".to_string())
2155                            })?;
2156
2157                            if let Some(vertex) = decoder.resolve_ref(start_attr)? {
2158                                // IfcVertexPoint has VertexGeometry (IfcCartesianPoint)
2159                                let point_attr = vertex.get(0).ok_or_else(|| {
2160                                    Error::geometry("VertexPoint missing geometry".to_string())
2161                                })?;
2162
2163                                if let Some(point) = decoder.resolve_ref(point_attr)? {
2164                                    let coords = point
2165                                        .get(0)
2166                                        .and_then(|v| v.as_list())
2167                                        .ok_or_else(|| {
2168                                            Error::geometry(
2169                                                "CartesianPoint missing coords".to_string(),
2170                                            )
2171                                        })?;
2172
2173                                    let x =
2174                                        coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
2175                                    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
2176                                    let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
2177
2178                                    polygon_points.push(Point3::new(x, y, z));
2179                                }
2180                            }
2181                        }
2182                    }
2183
2184                    // Triangulate the polygon
2185                    if polygon_points.len() >= 3 {
2186                        let base_idx = (positions.len() / 3) as u32;
2187
2188                        for point in &polygon_points {
2189                            positions.push(point.x as f32);
2190                            positions.push(point.y as f32);
2191                            positions.push(point.z as f32);
2192                        }
2193
2194                        // TODO: Fan triangulation assumes convex polygons. For non-convex faces,
2195                        // consider using triangulate_polygon_with_holes from FacetedBrepProcessor.
2196                        // Fan triangulation for simple convex polygons
2197                        for i in 1..polygon_points.len() - 1 {
2198                            indices.push(base_idx);
2199                            indices.push(base_idx + i as u32);
2200                            indices.push(base_idx + i as u32 + 1);
2201                        }
2202                    }
2203                }
2204            }
2205        }
2206
2207        Ok((positions, indices))
2208    }
2209
2210    /// Process a B-spline surface face
2211    fn process_bspline_face(
2212        &self,
2213        bspline: &DecodedEntity,
2214        decoder: &mut EntityDecoder,
2215    ) -> Result<(Vec<f32>, Vec<u32>)> {
2216        // Get degrees
2217        let u_degree = bspline.get_float(0).unwrap_or(3.0) as usize;
2218        let v_degree = bspline.get_float(1).unwrap_or(1.0) as usize;
2219
2220        // Parse control points
2221        let control_points = self.parse_control_points(bspline, decoder)?;
2222
2223        // Parse knot vectors
2224        let (u_knots, v_knots) = self.parse_knot_vectors(bspline)?;
2225
2226        // Determine tessellation resolution based on surface complexity
2227        let u_segments = (control_points.len() * 3).clamp(8, 24);
2228        let v_segments = if !control_points.is_empty() {
2229            (control_points[0].len() * 3).clamp(4, 24)
2230        } else {
2231            4
2232        };
2233
2234        // Tessellate the surface
2235        let (positions, indices) = Self::tessellate_bspline_surface(
2236            u_degree,
2237            v_degree,
2238            &control_points,
2239            &u_knots,
2240            &v_knots,
2241            u_segments,
2242            v_segments,
2243        );
2244
2245        Ok((positions, indices))
2246    }
2247}
2248
2249impl GeometryProcessor for AdvancedBrepProcessor {
2250    fn process(
2251        &self,
2252        entity: &DecodedEntity,
2253        decoder: &mut EntityDecoder,
2254        _schema: &IfcSchema,
2255    ) -> Result<Mesh> {
2256        // IfcAdvancedBrep attributes:
2257        // 0: Outer (IfcClosedShell)
2258
2259        // Get the outer shell
2260        let shell_attr = entity
2261            .get(0)
2262            .ok_or_else(|| Error::geometry("AdvancedBrep missing Outer shell".to_string()))?;
2263
2264        let shell = decoder
2265            .resolve_ref(shell_attr)?
2266            .ok_or_else(|| Error::geometry("Failed to resolve Outer shell".to_string()))?;
2267
2268        // Get faces from the shell (IfcClosedShell.CfsFaces)
2269        let faces_attr = shell
2270            .get(0)
2271            .ok_or_else(|| Error::geometry("ClosedShell missing CfsFaces".to_string()))?;
2272
2273        let faces = faces_attr
2274            .as_list()
2275            .ok_or_else(|| Error::geometry("Expected face list".to_string()))?;
2276
2277        let mut all_positions = Vec::new();
2278        let mut all_indices = Vec::new();
2279
2280        for face_ref in faces {
2281            if let Some(face_id) = face_ref.as_entity_ref() {
2282                let face = decoder.decode_by_id(face_id)?;
2283
2284                // IfcAdvancedFace has:
2285                // 0: Bounds (list of FaceBound)
2286                // 1: FaceSurface (IfcSurface - Plane, BSplineSurface, etc.)
2287                // 2: SameSense (boolean)
2288
2289                let surface_attr = face.get(1).ok_or_else(|| {
2290                    Error::geometry("AdvancedFace missing FaceSurface".to_string())
2291                })?;
2292
2293                let surface = decoder
2294                    .resolve_ref(surface_attr)?
2295                    .ok_or_else(|| Error::geometry("Failed to resolve FaceSurface".to_string()))?;
2296
2297                let surface_type = surface.ifc_type.as_str().to_uppercase();
2298                let (positions, indices) = if surface_type == "IFCPLANE" {
2299                    // Planar face - extract boundary vertices
2300                    self.process_planar_face(&face, decoder)?
2301                } else if surface_type == "IFCBSPLINESURFACEWITHKNOTS"
2302                    || surface_type == "IFCRATIONALBSPLINESURFACEWITHKNOTS"
2303                {
2304                    // B-spline surface - tessellate
2305                    self.process_bspline_face(&surface, decoder)?
2306                } else {
2307                    // Unsupported surface type - skip
2308                    continue;
2309                };
2310
2311                // Merge into combined mesh
2312                let base_idx = (all_positions.len() / 3) as u32;
2313                all_positions.extend(positions);
2314                for idx in indices {
2315                    all_indices.push(base_idx + idx);
2316                }
2317            }
2318        }
2319
2320        Ok(Mesh {
2321            positions: all_positions,
2322            normals: Vec::new(),
2323            indices: all_indices,
2324        })
2325    }
2326
2327    fn supported_types(&self) -> Vec<IfcType> {
2328        vec![IfcType::IfcAdvancedBrep, IfcType::IfcAdvancedBrepWithVoids]
2329    }
2330}
2331
2332impl Default for AdvancedBrepProcessor {
2333    fn default() -> Self {
2334        Self::new()
2335    }
2336}
2337
2338#[cfg(test)]
2339mod tests {
2340    use super::*;
2341
2342    #[test]
2343    fn test_advanced_brep_file() {
2344        use crate::router::GeometryRouter;
2345
2346        // Read the actual advanced_brep.ifc file
2347        let content =
2348            std::fs::read_to_string("../../tests/benchmark/models/ifcopenshell/advanced_brep.ifc")
2349                .expect("Failed to read test file");
2350
2351        let entity_index = ifc_lite_core::build_entity_index(&content);
2352        let mut decoder = EntityDecoder::with_index(&content, entity_index);
2353        let router = GeometryRouter::new();
2354
2355        // Process IFCBUILDINGELEMENTPROXY #181 which contains the AdvancedBrep geometry
2356        let element = decoder.decode_by_id(181).expect("Failed to decode element");
2357        assert_eq!(element.ifc_type, IfcType::IfcBuildingElementProxy);
2358
2359        let mesh = router
2360            .process_element(&element, &mut decoder)
2361            .expect("Failed to process advanced brep");
2362
2363        // Should produce geometry (B-spline surfaces tessellated)
2364        assert!(!mesh.is_empty(), "AdvancedBrep should produce geometry");
2365        assert!(
2366            mesh.positions.len() >= 3 * 100,
2367            "Should have significant geometry"
2368        );
2369        assert!(mesh.indices.len() >= 3 * 100, "Should have many triangles");
2370    }
2371
2372    #[test]
2373    fn test_extruded_area_solid() {
2374        let content = r#"
2375#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
2376#2=IFCDIRECTION((0.0,0.0,1.0));
2377#3=IFCEXTRUDEDAREASOLID(#1,$,#2,300.0);
2378"#;
2379
2380        let mut decoder = EntityDecoder::new(content);
2381        let schema = IfcSchema::new();
2382        let processor = ExtrudedAreaSolidProcessor::new(schema.clone());
2383
2384        let entity = decoder.decode_by_id(3).unwrap();
2385        let mesh = processor.process(&entity, &mut decoder, &schema).unwrap();
2386
2387        assert!(!mesh.is_empty());
2388        assert!(!mesh.positions.is_empty());
2389        assert!(!mesh.indices.is_empty());
2390    }
2391
2392    #[test]
2393    fn test_triangulated_face_set() {
2394        let content = r#"
2395#1=IFCCARTESIANPOINTLIST3D(((0.0,0.0,0.0),(100.0,0.0,0.0),(50.0,100.0,0.0)));
2396#2=IFCTRIANGULATEDFACESET(#1,$,$,((1,2,3)),$);
2397"#;
2398
2399        let mut decoder = EntityDecoder::new(content);
2400        let schema = IfcSchema::new();
2401        let processor = TriangulatedFaceSetProcessor::new();
2402
2403        let entity = decoder.decode_by_id(2).unwrap();
2404        let mesh = processor.process(&entity, &mut decoder, &schema).unwrap();
2405
2406        assert_eq!(mesh.positions.len(), 9); // 3 vertices * 3 coordinates
2407        assert_eq!(mesh.indices.len(), 3); // 1 triangle
2408    }
2409
2410    #[test]
2411    fn test_boolean_result_with_half_space() {
2412        // Simplified version of the 764--column.ifc structure
2413        let content = r#"
2414#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
2415#2=IFCDIRECTION((0.0,0.0,1.0));
2416#3=IFCEXTRUDEDAREASOLID(#1,$,#2,300.0);
2417#4=IFCCARTESIANPOINT((0.0,0.0,150.0));
2418#5=IFCDIRECTION((0.0,0.0,1.0));
2419#6=IFCAXIS2PLACEMENT3D(#4,#5,$);
2420#7=IFCPLANE(#6);
2421#8=IFCHALFSPACESOLID(#7,.T.);
2422#9=IFCBOOLEANRESULT(.DIFFERENCE.,#3,#8);
2423"#;
2424
2425        let mut decoder = EntityDecoder::new(content);
2426        let schema = IfcSchema::new();
2427        let processor = BooleanClippingProcessor::new();
2428
2429        // First verify the entity types are parsed correctly
2430        let bool_result = decoder.decode_by_id(9).unwrap();
2431        println!("BooleanResult type: {:?}", bool_result.ifc_type);
2432        assert_eq!(bool_result.ifc_type, IfcType::IfcBooleanResult);
2433
2434        let half_space = decoder.decode_by_id(8).unwrap();
2435        println!("HalfSpaceSolid type: {:?}", half_space.ifc_type);
2436        assert_eq!(half_space.ifc_type, IfcType::IfcHalfSpaceSolid);
2437
2438        // Now process the boolean result
2439        let mesh = processor
2440            .process(&bool_result, &mut decoder, &schema)
2441            .unwrap();
2442        println!("Mesh vertices: {}", mesh.positions.len() / 3);
2443        println!("Mesh triangles: {}", mesh.indices.len() / 3);
2444
2445        // The mesh should have geometry (base extrusion clipped)
2446        assert!(!mesh.is_empty(), "BooleanResult should produce geometry");
2447        assert!(!mesh.positions.is_empty());
2448    }
2449
2450    #[test]
2451    fn test_764_column_file() {
2452        use crate::router::GeometryRouter;
2453
2454        // Read the actual 764 column file
2455        let content = std::fs::read_to_string(
2456            "../../tests/benchmark/models/ifcopenshell/764--column--no-materials-or-surface-styles-found--augmented.ifc"
2457        ).expect("Failed to read test file");
2458
2459        let entity_index = ifc_lite_core::build_entity_index(&content);
2460        let mut decoder = EntityDecoder::with_index(&content, entity_index);
2461        let router = GeometryRouter::new();
2462
2463        // Decode IFCCOLUMN #8930
2464        let column = decoder.decode_by_id(8930).expect("Failed to decode column");
2465        println!("Column type: {:?}", column.ifc_type);
2466        assert_eq!(column.ifc_type, IfcType::IfcColumn);
2467
2468        // Check representation attribute
2469        let rep_attr = column
2470            .get(6)
2471            .expect("Column missing representation attribute");
2472        println!("Representation attr: {:?}", rep_attr);
2473
2474        // Try process_element
2475        match router.process_element(&column, &mut decoder) {
2476            Ok(mesh) => {
2477                println!("Mesh vertices: {}", mesh.positions.len() / 3);
2478                println!("Mesh triangles: {}", mesh.indices.len() / 3);
2479                assert!(!mesh.is_empty(), "Column should produce geometry");
2480            }
2481            Err(e) => {
2482                panic!("Failed to process column: {:?}", e);
2483            }
2484        }
2485    }
2486
2487    #[test]
2488    fn test_wall_with_opening_file() {
2489        use crate::router::GeometryRouter;
2490
2491        // Read the wall-with-opening file
2492        let content = std::fs::read_to_string(
2493            "../../tests/benchmark/models/buildingsmart/wall-with-opening-and-window.ifc",
2494        )
2495        .expect("Failed to read test file");
2496
2497        let entity_index = ifc_lite_core::build_entity_index(&content);
2498        let mut decoder = EntityDecoder::with_index(&content, entity_index);
2499        let router = GeometryRouter::new();
2500
2501        // Decode IFCWALL #45
2502        let wall = match decoder.decode_by_id(45) {
2503            Ok(w) => w,
2504            Err(e) => panic!("Failed to decode wall: {:?}", e),
2505        };
2506        println!("Wall type: {:?}", wall.ifc_type);
2507        assert_eq!(wall.ifc_type, IfcType::IfcWall);
2508
2509        // Check representation attribute (should be at index 6)
2510        let rep_attr = wall.get(6).expect("Wall missing representation attribute");
2511        println!("Representation attr: {:?}", rep_attr);
2512
2513        // Try process_element
2514        match router.process_element(&wall, &mut decoder) {
2515            Ok(mesh) => {
2516                println!("Wall mesh vertices: {}", mesh.positions.len() / 3);
2517                println!("Wall mesh triangles: {}", mesh.indices.len() / 3);
2518                assert!(!mesh.is_empty(), "Wall should produce geometry");
2519            }
2520            Err(e) => {
2521                panic!("Failed to process wall: {:?}", e);
2522            }
2523        }
2524
2525        // Also test window
2526        let window = decoder.decode_by_id(102).expect("Failed to decode window");
2527        println!("Window type: {:?}", window.ifc_type);
2528        assert_eq!(window.ifc_type, IfcType::IfcWindow);
2529
2530        match router.process_element(&window, &mut decoder) {
2531            Ok(mesh) => {
2532                println!("Window mesh vertices: {}", mesh.positions.len() / 3);
2533                println!("Window mesh triangles: {}", mesh.indices.len() / 3);
2534            }
2535            Err(e) => {
2536                println!("Window error (might be expected): {:?}", e);
2537            }
2538        }
2539    }
2540}