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