ifc_lite_geometry/
profiles.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//! Profile Processors - Handle all IFC profile types
6//!
7//! Dynamic profile processing for parametric, arbitrary, and composite profiles.
8
9use crate::{Error, Point2, Point3, Result, Vector3};
10use crate::profile::Profile2D;
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType, ProfileCategory};
12use std::f64::consts::PI;
13
14/// Profile processor - processes IFC profiles into 2D contours
15pub struct ProfileProcessor {
16    schema: IfcSchema,
17}
18
19impl ProfileProcessor {
20    /// Create new profile processor
21    pub fn new(schema: IfcSchema) -> Self {
22        Self { schema }
23    }
24
25    /// Process any IFC profile definition
26    #[inline]
27    pub fn process(
28        &self,
29        profile: &DecodedEntity,
30        decoder: &mut EntityDecoder,
31    ) -> Result<Profile2D> {
32        match self.schema.profile_category(&profile.ifc_type) {
33            Some(ProfileCategory::Parametric) => self.process_parametric(profile, decoder),
34            Some(ProfileCategory::Arbitrary) => self.process_arbitrary(profile, decoder),
35            Some(ProfileCategory::Composite) => self.process_composite(profile, decoder),
36            _ => Err(Error::geometry(format!(
37                "Unsupported profile type: {}",
38                profile.ifc_type
39            ))),
40        }
41    }
42
43    /// Process parametric profiles (rectangle, circle, I-shape, etc.)
44    #[inline]
45    fn process_parametric(
46        &self,
47        profile: &DecodedEntity,
48        decoder: &mut EntityDecoder,
49    ) -> Result<Profile2D> {
50        // First create the base profile shape
51        let mut base_profile = match profile.ifc_type {
52            IfcType::IfcRectangleProfileDef => self.process_rectangle(profile),
53            IfcType::IfcCircleProfileDef => self.process_circle(profile),
54            IfcType::IfcCircleHollowProfileDef => self.process_circle_hollow(profile),
55            IfcType::IfcRectangleHollowProfileDef => self.process_rectangle_hollow(profile),
56            IfcType::IfcIShapeProfileDef => self.process_i_shape(profile),
57            IfcType::IfcLShapeProfileDef => self.process_l_shape(profile),
58            IfcType::IfcUShapeProfileDef => self.process_u_shape(profile),
59            IfcType::IfcTShapeProfileDef => self.process_t_shape(profile),
60            IfcType::IfcCShapeProfileDef => self.process_c_shape(profile),
61            IfcType::IfcZShapeProfileDef => self.process_z_shape(profile),
62            _ => Err(Error::geometry(format!(
63                "Unsupported parametric profile: {}",
64                profile.ifc_type
65            ))),
66        }?;
67        
68        // Apply Profile Position transform (attribute 2: IfcAxis2Placement2D)
69        if let Some(pos_attr) = profile.get(2) {
70            if !pos_attr.is_null() {
71                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
72                    if pos_entity.ifc_type == IfcType::IfcAxis2Placement2D {
73                        self.apply_profile_position(&mut base_profile, &pos_entity, decoder)?;
74                    }
75                }
76            }
77        }
78        
79        Ok(base_profile)
80    }
81    
82    /// Apply IfcAxis2Placement2D transform to profile points
83    /// IfcAxis2Placement2D: Location, RefDirection
84    fn apply_profile_position(
85        &self,
86        profile: &mut Profile2D,
87        placement: &DecodedEntity,
88        decoder: &mut EntityDecoder,
89    ) -> Result<()> {
90        // Get Location (attribute 0) - IfcCartesianPoint
91        let (loc_x, loc_y) = if let Some(loc_attr) = placement.get(0) {
92            if !loc_attr.is_null() {
93                if let Some(loc_entity) = decoder.resolve_ref(loc_attr)? {
94                    let coords = loc_entity.get(0)
95                        .and_then(|v| v.as_list())
96                        .ok_or_else(|| Error::geometry("Missing point coordinates".to_string()))?;
97                    let x = coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
98                    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
99                    (x, y)
100                } else {
101                    (0.0, 0.0)
102                }
103            } else {
104                (0.0, 0.0)
105            }
106        } else {
107            (0.0, 0.0)
108        };
109        
110        // Get RefDirection (attribute 1) - IfcDirection (optional, default is (1,0))
111        let (dir_x, dir_y) = if let Some(dir_attr) = placement.get(1) {
112            if !dir_attr.is_null() {
113                if let Some(dir_entity) = decoder.resolve_ref(dir_attr)? {
114                    let ratios = dir_entity.get(0)
115                        .and_then(|v| v.as_list())
116                        .ok_or_else(|| Error::geometry("Missing direction ratios".to_string()))?;
117                    let x = ratios.get(0).and_then(|v| v.as_float()).unwrap_or(1.0);
118                    let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
119                    // Normalize
120                    let len = (x * x + y * y).sqrt();
121                    if len > 1e-10 {
122                        (x / len, y / len)
123                    } else {
124                        (1.0, 0.0)
125                    }
126                } else {
127                    (1.0, 0.0)
128                }
129            } else {
130                (1.0, 0.0)
131            }
132        } else {
133            (1.0, 0.0)
134        };
135        
136        // Skip transform if it's identity (location at origin, direction is (1,0))
137        if loc_x.abs() < 1e-10 && loc_y.abs() < 1e-10 && 
138           (dir_x - 1.0).abs() < 1e-10 && dir_y.abs() < 1e-10 {
139            return Ok(());
140        }
141        
142        // RefDirection is the local X axis direction
143        // Local Y axis is perpendicular: (-dir_y, dir_x)
144        let x_axis = (dir_x, dir_y);
145        let y_axis = (-dir_y, dir_x);
146        
147        // Transform all outer points
148        for point in &mut profile.outer {
149            let old_x = point.x;
150            let old_y = point.y;
151            // Rotation then translation: p' = R * p + t
152            point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
153            point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
154        }
155        
156        // Transform all hole points
157        for hole in &mut profile.holes {
158            for point in hole {
159                let old_x = point.x;
160                let old_y = point.y;
161                point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
162                point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
163            }
164        }
165        
166        Ok(())
167    }
168
169    /// Process rectangle profile
170    /// IfcRectangleProfileDef: ProfileType, ProfileName, Position, XDim, YDim
171    #[inline]
172    fn process_rectangle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
173        // Get dimensions (attributes 3 and 4)
174        let x_dim = profile
175            .get_float(3)
176            .ok_or_else(|| Error::geometry("Rectangle missing XDim".to_string()))?;
177        let y_dim = profile
178            .get_float(4)
179            .ok_or_else(|| Error::geometry("Rectangle missing YDim".to_string()))?;
180
181        // Create rectangle centered at origin
182        let half_x = x_dim / 2.0;
183        let half_y = y_dim / 2.0;
184
185        let points = vec![
186            Point2::new(-half_x, -half_y),
187            Point2::new(half_x, -half_y),
188            Point2::new(half_x, half_y),
189            Point2::new(-half_x, half_y),
190        ];
191
192        Ok(Profile2D::new(points))
193    }
194
195    /// Process circle profile
196    /// IfcCircleProfileDef: ProfileType, ProfileName, Position, Radius
197    #[inline]
198    fn process_circle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
199        // Get radius (attribute 3)
200        let radius = profile
201            .get_float(3)
202            .ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
203
204        // Generate circle with 24 segments (matches web-ifc typical quality)
205        let segments = 24;
206        let mut points = Vec::with_capacity(segments);
207
208        for i in 0..segments {
209            let angle = (i as f64) * 2.0 * PI / (segments as f64);
210            let x = radius * angle.cos();
211            let y = radius * angle.sin();
212            points.push(Point2::new(x, y));
213        }
214
215        Ok(Profile2D::new(points))
216    }
217
218    /// Process I-shape profile (simplified - basic I-beam)
219    /// IfcIShapeProfileDef: ProfileType, ProfileName, Position, OverallWidth, OverallDepth, WebThickness, FlangeThickness, ...
220    fn process_i_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
221        // Get dimensions
222        let overall_width = profile
223            .get_float(3)
224            .ok_or_else(|| Error::geometry("I-Shape missing OverallWidth".to_string()))?;
225        let overall_depth = profile
226            .get_float(4)
227            .ok_or_else(|| Error::geometry("I-Shape missing OverallDepth".to_string()))?;
228        let web_thickness = profile
229            .get_float(5)
230            .ok_or_else(|| Error::geometry("I-Shape missing WebThickness".to_string()))?;
231        let flange_thickness = profile
232            .get_float(6)
233            .ok_or_else(|| Error::geometry("I-Shape missing FlangeThickness".to_string()))?;
234
235        let half_width = overall_width / 2.0;
236        let half_depth = overall_depth / 2.0;
237        let half_web = web_thickness / 2.0;
238
239        // Create I-shape profile (counter-clockwise from bottom-left)
240        let points = vec![
241            // Bottom flange
242            Point2::new(-half_width, -half_depth),
243            Point2::new(half_width, -half_depth),
244            Point2::new(half_width, -half_depth + flange_thickness),
245            // Right side of web
246            Point2::new(half_web, -half_depth + flange_thickness),
247            Point2::new(half_web, half_depth - flange_thickness),
248            // Top flange
249            Point2::new(half_width, half_depth - flange_thickness),
250            Point2::new(half_width, half_depth),
251            Point2::new(-half_width, half_depth),
252            Point2::new(-half_width, half_depth - flange_thickness),
253            // Left side of web
254            Point2::new(-half_web, half_depth - flange_thickness),
255            Point2::new(-half_web, -half_depth + flange_thickness),
256            Point2::new(-half_width, -half_depth + flange_thickness),
257        ];
258
259        Ok(Profile2D::new(points))
260    }
261
262    /// Process circle hollow profile (tube/pipe)
263    /// IfcCircleHollowProfileDef: ProfileType, ProfileName, Position, Radius, WallThickness
264    fn process_circle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
265        let radius = profile
266            .get_float(3)
267            .ok_or_else(|| Error::geometry("CircleHollow missing Radius".to_string()))?;
268        let wall_thickness = profile
269            .get_float(4)
270            .ok_or_else(|| Error::geometry("CircleHollow missing WallThickness".to_string()))?;
271
272        let inner_radius = radius - wall_thickness;
273        let segments = 24;
274
275        // Outer circle
276        let mut outer_points = Vec::with_capacity(segments);
277        for i in 0..segments {
278            let angle = (i as f64) * 2.0 * PI / (segments as f64);
279            outer_points.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
280        }
281
282        // Inner circle (reversed for hole)
283        let mut inner_points = Vec::with_capacity(segments);
284        for i in (0..segments).rev() {
285            let angle = (i as f64) * 2.0 * PI / (segments as f64);
286            inner_points.push(Point2::new(inner_radius * angle.cos(), inner_radius * angle.sin()));
287        }
288
289        let mut result = Profile2D::new(outer_points);
290        result.add_hole(inner_points);
291        Ok(result)
292    }
293
294    /// Process rectangle hollow profile (rectangular tube)
295    /// IfcRectangleHollowProfileDef: ProfileType, ProfileName, Position, XDim, YDim, WallThickness, InnerFilletRadius, OuterFilletRadius
296    fn process_rectangle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
297        let x_dim = profile
298            .get_float(3)
299            .ok_or_else(|| Error::geometry("RectangleHollow missing XDim".to_string()))?;
300        let y_dim = profile
301            .get_float(4)
302            .ok_or_else(|| Error::geometry("RectangleHollow missing YDim".to_string()))?;
303        let wall_thickness = profile
304            .get_float(5)
305            .ok_or_else(|| Error::geometry("RectangleHollow missing WallThickness".to_string()))?;
306
307        let half_x = x_dim / 2.0;
308        let half_y = y_dim / 2.0;
309
310        // Validate wall thickness
311        if wall_thickness >= half_x || wall_thickness >= half_y {
312            return Err(Error::geometry(format!(
313                "RectangleHollow WallThickness {} exceeds half dimensions ({}, {})",
314                wall_thickness, half_x, half_y
315            )));
316        }
317
318        let inner_half_x = half_x - wall_thickness;
319        let inner_half_y = half_y - wall_thickness;
320
321        // Outer rectangle (counter-clockwise)
322        let outer_points = vec![
323            Point2::new(-half_x, -half_y),
324            Point2::new(half_x, -half_y),
325            Point2::new(half_x, half_y),
326            Point2::new(-half_x, half_y),
327        ];
328
329        // Inner rectangle (clockwise for hole - reversed order)
330        let inner_points = vec![
331            Point2::new(-inner_half_x, -inner_half_y),
332            Point2::new(-inner_half_x, inner_half_y),
333            Point2::new(inner_half_x, inner_half_y),
334            Point2::new(inner_half_x, -inner_half_y),
335        ];
336
337        let mut result = Profile2D::new(outer_points);
338        result.add_hole(inner_points);
339        Ok(result)
340    }
341
342    /// Process L-shape profile (angle)
343    /// IfcLShapeProfileDef: ProfileType, ProfileName, Position, Depth, Width, Thickness, ...
344    fn process_l_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
345        let depth = profile.get_float(3).ok_or_else(|| Error::geometry("L-Shape missing Depth".to_string()))?;
346        let width = profile.get_float(4).ok_or_else(|| Error::geometry("L-Shape missing Width".to_string()))?;
347        let thickness = profile.get_float(5).ok_or_else(|| Error::geometry("L-Shape missing Thickness".to_string()))?;
348
349        // L-shape profile (counter-clockwise from origin)
350        let points = vec![
351            Point2::new(0.0, 0.0),
352            Point2::new(width, 0.0),
353            Point2::new(width, thickness),
354            Point2::new(thickness, thickness),
355            Point2::new(thickness, depth),
356            Point2::new(0.0, depth),
357        ];
358
359        Ok(Profile2D::new(points))
360    }
361
362    /// Process U-shape profile (channel)
363    /// IfcUShapeProfileDef: ProfileType, ProfileName, Position, Depth, FlangeWidth, WebThickness, FlangeThickness, ...
364    fn process_u_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
365        let depth = profile.get_float(3).ok_or_else(|| Error::geometry("U-Shape missing Depth".to_string()))?;
366        let flange_width = profile.get_float(4).ok_or_else(|| Error::geometry("U-Shape missing FlangeWidth".to_string()))?;
367        let web_thickness = profile.get_float(5).ok_or_else(|| Error::geometry("U-Shape missing WebThickness".to_string()))?;
368        let flange_thickness = profile.get_float(6).ok_or_else(|| Error::geometry("U-Shape missing FlangeThickness".to_string()))?;
369
370        let half_depth = depth / 2.0;
371
372        // U-shape profile (counter-clockwise)
373        let points = vec![
374            Point2::new(0.0, -half_depth),
375            Point2::new(flange_width, -half_depth),
376            Point2::new(flange_width, -half_depth + flange_thickness),
377            Point2::new(web_thickness, -half_depth + flange_thickness),
378            Point2::new(web_thickness, half_depth - flange_thickness),
379            Point2::new(flange_width, half_depth - flange_thickness),
380            Point2::new(flange_width, half_depth),
381            Point2::new(0.0, half_depth),
382        ];
383
384        Ok(Profile2D::new(points))
385    }
386
387    /// Process T-shape profile
388    /// IfcTShapeProfileDef: ProfileType, ProfileName, Position, Depth, FlangeWidth, WebThickness, FlangeThickness, ...
389    fn process_t_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
390        let depth = profile.get_float(3).ok_or_else(|| Error::geometry("T-Shape missing Depth".to_string()))?;
391        let flange_width = profile.get_float(4).ok_or_else(|| Error::geometry("T-Shape missing FlangeWidth".to_string()))?;
392        let web_thickness = profile.get_float(5).ok_or_else(|| Error::geometry("T-Shape missing WebThickness".to_string()))?;
393        let flange_thickness = profile.get_float(6).ok_or_else(|| Error::geometry("T-Shape missing FlangeThickness".to_string()))?;
394
395        let half_flange = flange_width / 2.0;
396        let half_web = web_thickness / 2.0;
397
398        // T-shape profile (counter-clockwise)
399        let points = vec![
400            Point2::new(-half_web, 0.0),
401            Point2::new(-half_web, depth - flange_thickness),
402            Point2::new(-half_flange, depth - flange_thickness),
403            Point2::new(-half_flange, depth),
404            Point2::new(half_flange, depth),
405            Point2::new(half_flange, depth - flange_thickness),
406            Point2::new(half_web, depth - flange_thickness),
407            Point2::new(half_web, 0.0),
408        ];
409
410        Ok(Profile2D::new(points))
411    }
412
413    /// Process C-shape profile (channel with lips)
414    /// IfcCShapeProfileDef: ProfileType, ProfileName, Position, Depth, Width, WallThickness, Girth, ...
415    fn process_c_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
416        let depth = profile.get_float(3).ok_or_else(|| Error::geometry("C-Shape missing Depth".to_string()))?;
417        let width = profile.get_float(4).ok_or_else(|| Error::geometry("C-Shape missing Width".to_string()))?;
418        let wall_thickness = profile.get_float(5).ok_or_else(|| Error::geometry("C-Shape missing WallThickness".to_string()))?;
419        let girth = profile.get_float(6).unwrap_or(wall_thickness * 2.0); // Lip length
420
421        let half_depth = depth / 2.0;
422
423        // C-shape profile (counter-clockwise)
424        let points = vec![
425            Point2::new(girth, -half_depth),
426            Point2::new(0.0, -half_depth),
427            Point2::new(0.0, half_depth),
428            Point2::new(girth, half_depth),
429            Point2::new(girth, half_depth - wall_thickness),
430            Point2::new(wall_thickness, half_depth - wall_thickness),
431            Point2::new(wall_thickness, -half_depth + wall_thickness),
432            Point2::new(girth, -half_depth + wall_thickness),
433        ];
434
435        Ok(Profile2D::new(points))
436    }
437
438    /// Process Z-shape profile
439    /// IfcZShapeProfileDef: ProfileType, ProfileName, Position, Depth, FlangeWidth, WebThickness, FlangeThickness, ...
440    fn process_z_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
441        let depth = profile.get_float(3).ok_or_else(|| Error::geometry("Z-Shape missing Depth".to_string()))?;
442        let flange_width = profile.get_float(4).ok_or_else(|| Error::geometry("Z-Shape missing FlangeWidth".to_string()))?;
443        let web_thickness = profile.get_float(5).ok_or_else(|| Error::geometry("Z-Shape missing WebThickness".to_string()))?;
444        let flange_thickness = profile.get_float(6).ok_or_else(|| Error::geometry("Z-Shape missing FlangeThickness".to_string()))?;
445
446        let half_depth = depth / 2.0;
447        let half_web = web_thickness / 2.0;
448
449        // Z-shape profile (counter-clockwise)
450        let points = vec![
451            Point2::new(-half_web, -half_depth),
452            Point2::new(-half_web - flange_width, -half_depth),
453            Point2::new(-half_web - flange_width, -half_depth + flange_thickness),
454            Point2::new(-half_web, -half_depth + flange_thickness),
455            Point2::new(-half_web, half_depth - flange_thickness),
456            Point2::new(half_web, half_depth - flange_thickness),
457            Point2::new(half_web, half_depth),
458            Point2::new(half_web + flange_width, half_depth),
459            Point2::new(half_web + flange_width, half_depth - flange_thickness),
460            Point2::new(half_web, half_depth - flange_thickness),
461            Point2::new(half_web, -half_depth + flange_thickness),
462            Point2::new(-half_web, -half_depth + flange_thickness),
463        ];
464
465        Ok(Profile2D::new(points))
466    }
467
468    /// Process arbitrary closed profile (polyline-based)
469    /// IfcArbitraryClosedProfileDef: ProfileType, ProfileName, OuterCurve
470    /// IfcArbitraryProfileDefWithVoids: ProfileType, ProfileName, OuterCurve, InnerCurves
471    fn process_arbitrary(
472        &self,
473        profile: &DecodedEntity,
474        decoder: &mut EntityDecoder,
475    ) -> Result<Profile2D> {
476        // Get outer curve (attribute 2)
477        let curve_attr = profile
478            .get(2)
479            .ok_or_else(|| Error::geometry("Arbitrary profile missing OuterCurve".to_string()))?;
480
481        let curve = decoder
482            .resolve_ref(curve_attr)?
483            .ok_or_else(|| Error::geometry("Failed to resolve OuterCurve".to_string()))?;
484
485        // Process outer curve
486        let outer_points = self.process_curve(&curve, decoder)?;
487        let mut result = Profile2D::new(outer_points);
488
489        // Check if this is IfcArbitraryProfileDefWithVoids (has inner curves)
490        if profile.ifc_type == IfcType::IfcArbitraryProfileDefWithVoids {
491            // Get inner curves list (attribute 3)
492            if let Some(inner_curves_attr) = profile.get(3) {
493                let inner_curves = decoder.resolve_ref_list(inner_curves_attr)?;
494                for inner_curve in inner_curves {
495                    let hole_points = self.process_curve(&inner_curve, decoder)?;
496                    result.add_hole(hole_points);
497                }
498            }
499        }
500
501        Ok(result)
502    }
503
504    /// Process any supported curve type into 2D points
505    #[inline]
506    fn process_curve(
507        &self,
508        curve: &DecodedEntity,
509        decoder: &mut EntityDecoder,
510    ) -> Result<Vec<Point2<f64>>> {
511        match curve.ifc_type {
512            IfcType::IfcPolyline => self.process_polyline(curve, decoder),
513            IfcType::IfcIndexedPolyCurve => self.process_indexed_polycurve(curve, decoder),
514            IfcType::IfcCompositeCurve => self.process_composite_curve(curve, decoder),
515            IfcType::IfcTrimmedCurve => self.process_trimmed_curve(curve, decoder),
516            IfcType::IfcCircle => self.process_circle_curve(curve, decoder),
517            IfcType::IfcEllipse => self.process_ellipse_curve(curve, decoder),
518            _ => Err(Error::geometry(format!(
519                "Unsupported curve type: {}",
520                curve.ifc_type
521            ))),
522        }
523    }
524
525    /// Get 3D points from a curve (for swept disk solid, etc.)
526    #[inline]
527    pub fn get_curve_points(
528        &self,
529        curve: &DecodedEntity,
530        decoder: &mut EntityDecoder,
531    ) -> Result<Vec<Point3<f64>>> {
532        match curve.ifc_type {
533            IfcType::IfcPolyline => self.process_polyline_3d(curve, decoder),
534            IfcType::IfcCompositeCurve => self.process_composite_curve_3d(curve, decoder),
535            IfcType::IfcCircle => self.process_circle_3d(curve, decoder),
536            IfcType::IfcTrimmedCurve => {
537                // For trimmed curve, get 2D points and convert to 3D
538                let points_2d = self.process_trimmed_curve(curve, decoder)?;
539                Ok(points_2d.into_iter().map(|p| Point3::new(p.x, p.y, 0.0)).collect())
540            }
541            _ => {
542                // Fallback: try 2D curve and convert to 3D
543                let points_2d = self.process_curve(curve, decoder)?;
544                Ok(points_2d.into_iter().map(|p| Point3::new(p.x, p.y, 0.0)).collect())
545            }
546        }
547    }
548
549    /// Process circle curve in 3D space (for swept disk solid, etc.)
550    fn process_circle_3d(
551        &self,
552        curve: &DecodedEntity,
553        decoder: &mut EntityDecoder,
554    ) -> Result<Vec<Point3<f64>>> {
555        // IfcCircle: Position (IfcAxis2Placement2D or 3D), Radius
556        let position_attr = curve.get(0).ok_or_else(|| {
557            Error::geometry("Circle missing Position".to_string())
558        })?;
559
560        let radius = curve.get_float(1).ok_or_else(|| {
561            Error::geometry("Circle missing Radius".to_string())
562        })?;
563
564        let position = decoder.resolve_ref(position_attr)?.ok_or_else(|| {
565            Error::geometry("Failed to resolve circle position".to_string())
566        })?;
567
568        // Get center and orientation from Axis2Placement3D
569        let (center, x_axis, y_axis) = if position.ifc_type == IfcType::IfcAxis2Placement3D {
570            // IfcAxis2Placement3D: Location, Axis (Z), RefDirection (X)
571            let loc_attr = position.get(0).ok_or_else(|| {
572                Error::geometry("Axis2Placement3D missing Location".to_string())
573            })?;
574            let loc = decoder.resolve_ref(loc_attr)?.ok_or_else(|| {
575                Error::geometry("Failed to resolve location".to_string())
576            })?;
577            let coords = loc.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
578                Error::geometry("Location missing coordinates".to_string())
579            })?;
580            let center = Point3::new(
581                coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0),
582                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
583                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
584            );
585
586            // Get Z axis (Axis attribute)
587            let z_axis = if let Some(axis_attr) = position.get(1) {
588                if !axis_attr.is_null() {
589                    let axis = decoder.resolve_ref(axis_attr)?;
590                    if let Some(axis) = axis {
591                        let coords = axis.get(0).and_then(|v| v.as_list());
592                        if let Some(coords) = coords {
593                            Vector3::new(
594                                coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0),
595                                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
596                                coords.get(2).and_then(|v| v.as_float()).unwrap_or(1.0),
597                            ).normalize()
598                        } else {
599                            Vector3::new(0.0, 0.0, 1.0)
600                        }
601                    } else {
602                        Vector3::new(0.0, 0.0, 1.0)
603                    }
604                } else {
605                    Vector3::new(0.0, 0.0, 1.0)
606                }
607            } else {
608                Vector3::new(0.0, 0.0, 1.0)
609            };
610
611            // Get X axis (RefDirection attribute)
612            let x_axis = if let Some(ref_attr) = position.get(2) {
613                if !ref_attr.is_null() {
614                    let ref_dir = decoder.resolve_ref(ref_attr)?;
615                    if let Some(ref_dir) = ref_dir {
616                        let coords = ref_dir.get(0).and_then(|v| v.as_list());
617                        if let Some(coords) = coords {
618                            Vector3::new(
619                                coords.get(0).and_then(|v| v.as_float()).unwrap_or(1.0),
620                                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
621                                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
622                            ).normalize()
623                        } else {
624                            Vector3::new(1.0, 0.0, 0.0)
625                        }
626                    } else {
627                        Vector3::new(1.0, 0.0, 0.0)
628                    }
629                } else {
630                    Vector3::new(1.0, 0.0, 0.0)
631                }
632            } else {
633                Vector3::new(1.0, 0.0, 0.0)
634            };
635
636            // Y axis = Z cross X
637            let y_axis = z_axis.cross(&x_axis).normalize();
638
639            (center, x_axis, y_axis)
640        } else {
641            // 2D placement - use XY plane
642            let loc_attr = position.get(0);
643            let (cx, cy) = if let Some(attr) = loc_attr {
644                let loc = decoder.resolve_ref(attr)?;
645                if let Some(loc) = loc {
646                    let coords = loc.get(0).and_then(|v| v.as_list());
647                    if let Some(coords) = coords {
648                        (
649                            coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0),
650                            coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
651                        )
652                    } else {
653                        (0.0, 0.0)
654                    }
655                } else {
656                    (0.0, 0.0)
657                }
658            } else {
659                (0.0, 0.0)
660            };
661            (
662                Point3::new(cx, cy, 0.0),
663                Vector3::new(1.0, 0.0, 0.0),
664                Vector3::new(0.0, 1.0, 0.0),
665            )
666        };
667
668        // Generate circle points in 3D (16 segments for full circle)
669        let segments = 16usize;
670        let mut points = Vec::with_capacity(segments + 1);
671
672        for i in 0..=segments {
673            let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
674            let p = center + x_axis * (radius * angle.cos()) + y_axis * (radius * angle.sin());
675            points.push(p);
676        }
677
678        Ok(points)
679    }
680
681    /// Process polyline into 3D points
682    fn process_polyline_3d(
683        &self,
684        curve: &DecodedEntity,
685        decoder: &mut EntityDecoder,
686    ) -> Result<Vec<Point3<f64>>> {
687        // IfcPolyline: Points
688        let points_attr = curve.get(0).ok_or_else(|| {
689            Error::geometry("Polyline missing Points".to_string())
690        })?;
691
692        let points = decoder.resolve_ref_list(points_attr)?;
693        let mut result = Vec::with_capacity(points.len());
694
695        for point in points {
696            // IfcCartesianPoint: Coordinates
697            let coords_attr = point.get(0).ok_or_else(|| {
698                Error::geometry("CartesianPoint missing Coordinates".to_string())
699            })?;
700
701            let coords = coords_attr.as_list().ok_or_else(|| {
702                Error::geometry("Coordinates is not a list".to_string())
703            })?;
704
705            let x = coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
706            let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
707            let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
708
709            result.push(Point3::new(x, y, z));
710        }
711
712        Ok(result)
713    }
714
715    /// Process composite curve into 3D points
716    fn process_composite_curve_3d(
717        &self,
718        curve: &DecodedEntity,
719        decoder: &mut EntityDecoder,
720    ) -> Result<Vec<Point3<f64>>> {
721        // IfcCompositeCurve: Segments, SelfIntersect
722        let segments_attr = curve.get(0).ok_or_else(|| {
723            Error::geometry("CompositeCurve missing Segments".to_string())
724        })?;
725
726        let segments = decoder.resolve_ref_list(segments_attr)?;
727        let mut result = Vec::new();
728
729        for segment in segments {
730            // IfcCompositeCurveSegment: Transition, SameSense, ParentCurve
731            let parent_curve_attr = segment.get(2).ok_or_else(|| {
732                Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
733            })?;
734
735            let parent_curve = decoder.resolve_ref(parent_curve_attr)?.ok_or_else(|| {
736                Error::geometry("Failed to resolve ParentCurve".to_string())
737            })?;
738
739            // Get same_sense for direction
740            let same_sense = segment.get(1)
741                .and_then(|v| match v {
742                    ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
743                    _ => None,
744                })
745                .map(|e| e == "T" || e == "TRUE")
746                .unwrap_or(true);
747
748            let mut segment_points = self.get_curve_points(&parent_curve, decoder)?;
749
750            if !same_sense {
751                segment_points.reverse();
752            }
753
754            // Skip first point if we already have points (avoid duplicates)
755            if !result.is_empty() && !segment_points.is_empty() {
756                result.extend(segment_points.into_iter().skip(1));
757            } else {
758                result.extend(segment_points);
759            }
760        }
761
762        Ok(result)
763    }
764
765    /// Process trimmed curve
766    /// IfcTrimmedCurve: BasisCurve, Trim1, Trim2, SenseAgreement, MasterRepresentation
767    fn process_trimmed_curve(
768        &self,
769        curve: &DecodedEntity,
770        decoder: &mut EntityDecoder,
771    ) -> Result<Vec<Point2<f64>>> {
772        // Get basis curve (attribute 0)
773        let basis_attr = curve
774            .get(0)
775            .ok_or_else(|| Error::geometry("TrimmedCurve missing BasisCurve".to_string()))?;
776
777        let basis_curve = decoder
778            .resolve_ref(basis_attr)?
779            .ok_or_else(|| Error::geometry("Failed to resolve BasisCurve".to_string()))?;
780
781        // Get trim parameters
782        let trim1 = curve.get(1).and_then(|v| self.extract_trim_param(v));
783        let trim2 = curve.get(2).and_then(|v| self.extract_trim_param(v));
784
785        // Get sense agreement (attribute 3) - default true
786        let sense = curve
787            .get(3)
788            .and_then(|v| match v {
789                ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T"),
790                _ => None,
791            })
792            .unwrap_or(true);
793
794        // Process basis curve based on type
795        match basis_curve.ifc_type {
796            IfcType::IfcCircle | IfcType::IfcEllipse => {
797                self.process_trimmed_conic(&basis_curve, trim1, trim2, sense, decoder)
798            }
799            _ => {
800                // Fallback: try to process as a regular curve
801                self.process_curve(&basis_curve, decoder)
802            }
803        }
804    }
805
806    /// Extract trim parameter (can be IFCPARAMETERVALUE or IFCCARTESIANPOINT)
807    fn extract_trim_param(&self, attr: &ifc_lite_core::AttributeValue) -> Option<f64> {
808        if let Some(list) = attr.as_list() {
809            for item in list {
810                // Check for IFCPARAMETERVALUE (stored as ["IFCPARAMETERVALUE", value])
811                if let Some(inner_list) = item.as_list() {
812                    if inner_list.len() >= 2 {
813                        if let Some(type_name) = inner_list.get(0).and_then(|v| v.as_string()) {
814                            if type_name == "IFCPARAMETERVALUE" {
815                                return inner_list.get(1).and_then(|v| v.as_float());
816                            }
817                        }
818                    }
819                }
820                if let Some(f) = item.as_float() {
821                    return Some(f);
822                }
823            }
824        }
825        None
826    }
827
828    /// Process trimmed conic (circle or ellipse arc)
829    fn process_trimmed_conic(
830        &self,
831        basis: &DecodedEntity,
832        trim1: Option<f64>,
833        trim2: Option<f64>,
834        sense: bool,
835        decoder: &mut EntityDecoder,
836    ) -> Result<Vec<Point2<f64>>> {
837        let radius = basis.get_float(1).unwrap_or(1.0);
838        let radius2 = if basis.ifc_type == IfcType::IfcEllipse {
839            basis.get_float(2).unwrap_or(radius)
840        } else {
841            radius
842        };
843
844        let (center, rotation) = self.get_placement_2d(basis, decoder)?;
845
846        // Convert trim parameters to angles (in degrees usually)
847        let start_angle = trim1.unwrap_or(0.0).to_radians();
848        let end_angle = trim2.unwrap_or(360.0).to_radians();
849
850        // Calculate arc angle and adaptive segment count
851        // Use ~8 segments per 90° (quarter circle), minimum 2
852        let arc_angle = (end_angle - start_angle).abs();
853        let num_segments = ((arc_angle / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize).max(2);
854        let mut points = Vec::with_capacity(num_segments + 1);
855
856        let angle_range = if sense {
857            end_angle - start_angle
858        } else {
859            start_angle - end_angle
860        };
861
862        for i in 0..=num_segments {
863            let t = i as f64 / num_segments as f64;
864            let angle = if sense {
865                start_angle + t * angle_range
866            } else {
867                start_angle - t * angle_range.abs()
868            };
869
870            let x = radius * angle.cos();
871            let y = radius2 * angle.sin();
872
873            let rx = x * rotation.cos() - y * rotation.sin() + center.x;
874            let ry = x * rotation.sin() + y * rotation.cos() + center.y;
875
876            points.push(Point2::new(rx, ry));
877        }
878
879        Ok(points)
880    }
881
882    /// Get 2D placement from entity
883    fn get_placement_2d(
884        &self,
885        entity: &DecodedEntity,
886        decoder: &mut EntityDecoder,
887    ) -> Result<(Point2<f64>, f64)> {
888        let placement_attr = match entity.get(0) {
889            Some(attr) if !attr.is_null() => attr,
890            _ => return Ok((Point2::new(0.0, 0.0), 0.0)),
891        };
892
893        let placement = match decoder.resolve_ref(placement_attr)? {
894            Some(p) => p,
895            None => return Ok((Point2::new(0.0, 0.0), 0.0)),
896        };
897
898        let location_attr = placement.get(0);
899        let center = if let Some(loc_attr) = location_attr {
900            if let Some(loc) = decoder.resolve_ref(loc_attr)? {
901                let coords = loc.get(0).and_then(|v| v.as_list());
902                if let Some(coords) = coords {
903                    let x = coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
904                    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
905                    Point2::new(x, y)
906                } else {
907                    Point2::new(0.0, 0.0)
908                }
909            } else {
910                Point2::new(0.0, 0.0)
911            }
912        } else {
913            Point2::new(0.0, 0.0)
914        };
915
916        let rotation = if let Some(dir_attr) = placement.get(1) {
917            if let Some(dir) = decoder.resolve_ref(dir_attr)? {
918                let ratios = dir.get(0).and_then(|v| v.as_list());
919                if let Some(ratios) = ratios {
920                    let x = ratios.get(0).and_then(|v| v.as_float()).unwrap_or(1.0);
921                    let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
922                    y.atan2(x)
923                } else {
924                    0.0
925                }
926            } else {
927                0.0
928            }
929        } else {
930            0.0
931        };
932
933        Ok((center, rotation))
934    }
935
936    /// Process circle curve (full circle)
937    fn process_circle_curve(
938        &self,
939        curve: &DecodedEntity,
940        decoder: &mut EntityDecoder,
941    ) -> Result<Vec<Point2<f64>>> {
942        let radius = curve.get_float(1).unwrap_or(1.0);
943        let (center, rotation) = self.get_placement_2d(curve, decoder)?;
944
945        let segments = 24;
946        let mut points = Vec::with_capacity(segments);
947
948        for i in 0..segments {
949            let angle = (i as f64) * 2.0 * PI / (segments as f64);
950            let x = radius * angle.cos();
951            let y = radius * angle.sin();
952
953            let rx = x * rotation.cos() - y * rotation.sin() + center.x;
954            let ry = x * rotation.sin() + y * rotation.cos() + center.y;
955
956            points.push(Point2::new(rx, ry));
957        }
958
959        Ok(points)
960    }
961
962    /// Process ellipse curve (full ellipse)
963    fn process_ellipse_curve(
964        &self,
965        curve: &DecodedEntity,
966        decoder: &mut EntityDecoder,
967    ) -> Result<Vec<Point2<f64>>> {
968        let semi_axis1 = curve.get_float(1).unwrap_or(1.0);
969        let semi_axis2 = curve.get_float(2).unwrap_or(1.0);
970        let (center, rotation) = self.get_placement_2d(curve, decoder)?;
971
972        let segments = 24;
973        let mut points = Vec::with_capacity(segments);
974
975        for i in 0..segments {
976            let angle = (i as f64) * 2.0 * PI / (segments as f64);
977            let x = semi_axis1 * angle.cos();
978            let y = semi_axis2 * angle.sin();
979
980            let rx = x * rotation.cos() - y * rotation.sin() + center.x;
981            let ry = x * rotation.sin() + y * rotation.cos() + center.y;
982
983            points.push(Point2::new(rx, ry));
984        }
985
986        Ok(points)
987    }
988
989    /// Process polyline into 2D points
990    /// IfcPolyline: Points (list of IfcCartesianPoint)
991    #[inline]
992    fn process_polyline(
993        &self,
994        polyline: &DecodedEntity,
995        decoder: &mut EntityDecoder,
996    ) -> Result<Vec<Point2<f64>>> {
997        // Get points list (attribute 0)
998        let points_attr = polyline
999            .get(0)
1000            .ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
1001
1002        let point_entities = decoder.resolve_ref_list(points_attr)?;
1003
1004        let mut points = Vec::with_capacity(point_entities.len());
1005        for point_entity in point_entities {
1006            if point_entity.ifc_type != IfcType::IfcCartesianPoint {
1007                continue;
1008            }
1009
1010            // Get coordinates (attribute 0)
1011            let coords_attr = point_entity
1012                .get(0)
1013                .ok_or_else(|| Error::geometry("CartesianPoint missing coordinates".to_string()))?;
1014
1015            let coords = coords_attr
1016                .as_list()
1017                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
1018
1019            let x = coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
1020            let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
1021
1022            points.push(Point2::new(x, y));
1023        }
1024
1025        Ok(points)
1026    }
1027
1028    /// Process indexed polycurve into 2D points
1029    /// IfcIndexedPolyCurve: Points (IfcCartesianPointList2D), Segments (optional), SelfIntersect
1030    fn process_indexed_polycurve(
1031        &self,
1032        curve: &DecodedEntity,
1033        decoder: &mut EntityDecoder,
1034    ) -> Result<Vec<Point2<f64>>> {
1035        // Get points list (attribute 0) - references IfcCartesianPointList2D
1036        let points_attr = curve
1037            .get(0)
1038            .ok_or_else(|| Error::geometry("IndexedPolyCurve missing Points".to_string()))?;
1039
1040        let points_list = decoder
1041            .resolve_ref(points_attr)?
1042            .ok_or_else(|| Error::geometry("Failed to resolve Points list".to_string()))?;
1043
1044        // IfcCartesianPointList2D: CoordList (list of 2D coordinates)
1045        let coord_list_attr = points_list
1046            .get(0)
1047            .ok_or_else(|| Error::geometry("CartesianPointList2D missing CoordList".to_string()))?;
1048
1049        let coord_list = coord_list_attr
1050            .as_list()
1051            .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
1052
1053        // Parse all 2D points from the coordinate list
1054        let all_points: Vec<Point2<f64>> = coord_list
1055            .iter()
1056            .filter_map(|coord| {
1057                coord.as_list().and_then(|coords| {
1058                    let x = coords.get(0)?.as_float()?;
1059                    let y = coords.get(1)?.as_float()?;
1060                    Some(Point2::new(x, y))
1061                })
1062            })
1063            .collect();
1064
1065        // Get segments (attribute 1) - optional, if not present use all points in order
1066        let segments_attr = curve.get(1);
1067
1068        if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
1069            // No segments specified - use all points in order
1070            return Ok(all_points);
1071        }
1072
1073        // Process segments (IfcLineIndex or IfcArcIndex)
1074        let segments = segments_attr
1075            .unwrap()
1076            .as_list()
1077            .ok_or_else(|| Error::geometry("Expected segments list".to_string()))?;
1078
1079        let mut result_points = Vec::new();
1080
1081        for segment in segments {
1082            // Each segment is either IFCLINEINDEX((i1,i2,...)) or IFCARCINDEX((i1,i2,i3))
1083            // The segment itself contains a list of indices
1084            if let Some(indices) = segment.as_list() {
1085                let idx_values: Vec<usize> = indices
1086                    .iter()
1087                    .filter_map(|v| v.as_float().map(|f| f as usize - 1)) // 1-indexed to 0-indexed
1088                    .collect();
1089
1090                if idx_values.len() == 3 {
1091                    // Arc segment - 3 points define an arc
1092                    let p1 = all_points.get(idx_values[0]).copied();
1093                    let p2 = all_points.get(idx_values[1]).copied(); // Mid-point
1094                    let p3 = all_points.get(idx_values[2]).copied();
1095
1096                    if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
1097                        // Approximate arc with adaptive segment count based on arc size
1098                        // Calculate approximate arc angle from chord length vs radius
1099                        let chord_len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
1100                        let mid_chord = ((mid.x - (start.x + end.x) / 2.0).powi(2) + (mid.y - (start.y + end.y) / 2.0).powi(2)).sqrt();
1101                        // Estimate arc angle: larger mid deviation = larger arc
1102                        let arc_estimate = if chord_len > 1e-10 { (mid_chord / chord_len).abs().min(1.0).acos() * 2.0 } else { 0.5 };
1103                        let num_segments = ((arc_estimate / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize).max(4).min(16);
1104                        let arc_points = self.approximate_arc_3pt(start, mid, end, num_segments);
1105                        for pt in arc_points {
1106                            if result_points.last() != Some(&pt) {
1107                                result_points.push(pt);
1108                            }
1109                        }
1110                    }
1111                } else {
1112                    // Line segment - add all points
1113                    for &idx in &idx_values {
1114                        if let Some(&pt) = all_points.get(idx) {
1115                            if result_points.last() != Some(&pt) {
1116                                result_points.push(pt);
1117                            }
1118                        }
1119                    }
1120                }
1121            }
1122        }
1123
1124        Ok(result_points)
1125    }
1126
1127    /// Approximate a 3-point arc with line segments
1128    fn approximate_arc_3pt(
1129        &self,
1130        p1: Point2<f64>,
1131        p2: Point2<f64>,
1132        p3: Point2<f64>,
1133        num_segments: usize,
1134    ) -> Vec<Point2<f64>> {
1135        // Find circle center from 3 points
1136        let ax = p1.x;
1137        let ay = p1.y;
1138        let bx = p2.x;
1139        let by = p2.y;
1140        let cx = p3.x;
1141        let cy = p3.y;
1142
1143        let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
1144
1145        if d.abs() < 1e-10 {
1146            // Points are collinear - return as line
1147            return vec![p1, p2, p3];
1148        }
1149
1150        let ux = ((ax * ax + ay * ay) * (by - cy)
1151            + (bx * bx + by * by) * (cy - ay)
1152            + (cx * cx + cy * cy) * (ay - by))
1153            / d;
1154        let uy = ((ax * ax + ay * ay) * (cx - bx)
1155            + (bx * bx + by * by) * (ax - cx)
1156            + (cx * cx + cy * cy) * (bx - ax))
1157            / d;
1158
1159        let center = Point2::new(ux, uy);
1160        let radius = ((p1.x - center.x).powi(2) + (p1.y - center.y).powi(2)).sqrt();
1161
1162        // Calculate angles
1163        let angle1 = (p1.y - center.y).atan2(p1.x - center.x);
1164        let angle3 = (p3.y - center.y).atan2(p3.x - center.x);
1165        let angle2 = (p2.y - center.y).atan2(p2.x - center.x);
1166
1167        // Determine arc direction
1168        let mut start_angle = angle1;
1169        let mut end_angle = angle3;
1170
1171        // Check if we need to go the long way around
1172        let mid_check = angle1 + (angle3 - angle1) / 2.0;
1173        let diff = (angle2 - mid_check).abs();
1174        if diff > PI {
1175            // Go the other way
1176            if end_angle > start_angle {
1177                end_angle -= 2.0 * PI;
1178            } else {
1179                end_angle += 2.0 * PI;
1180            }
1181        }
1182
1183        // Generate arc points
1184        let mut points = Vec::with_capacity(num_segments + 1);
1185        for i in 0..=num_segments {
1186            let t = i as f64 / num_segments as f64;
1187            let angle = start_angle + t * (end_angle - start_angle);
1188            points.push(Point2::new(
1189                center.x + radius * angle.cos(),
1190                center.y + radius * angle.sin(),
1191            ));
1192        }
1193
1194        points
1195    }
1196
1197    /// Process composite curve into 2D points
1198    /// IfcCompositeCurve: Segments (list of IfcCompositeCurveSegment), SelfIntersect
1199    fn process_composite_curve(
1200        &self,
1201        curve: &DecodedEntity,
1202        decoder: &mut EntityDecoder,
1203    ) -> Result<Vec<Point2<f64>>> {
1204        // Get segments list (attribute 0)
1205        let segments_attr = curve
1206            .get(0)
1207            .ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
1208
1209        let segments = decoder.resolve_ref_list(segments_attr)?;
1210
1211        let mut all_points = Vec::new();
1212
1213        for segment in segments {
1214            // IfcCompositeCurveSegment: Transition, SameSense, ParentCurve
1215            if segment.ifc_type != IfcType::IfcCompositeCurveSegment {
1216                continue;
1217            }
1218
1219            // Get ParentCurve (attribute 2)
1220            let parent_curve_attr = segment
1221                .get(2)
1222                .ok_or_else(|| Error::geometry("CompositeCurveSegment missing ParentCurve".to_string()))?;
1223
1224            let parent_curve = decoder
1225                .resolve_ref(parent_curve_attr)?
1226                .ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
1227
1228            // Get SameSense (attribute 1) - whether to reverse the curve
1229            // Note: IFC enum values like ".T." are parsed/stored as "T" without dots
1230            let same_sense = segment
1231                .get(1)
1232                .and_then(|v| match v {
1233                    ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T" || s == "TRUE"),
1234                    _ => None,
1235                })
1236                .unwrap_or(true);
1237
1238            // Process the parent curve
1239            let mut segment_points = self.process_curve(&parent_curve, decoder)?;
1240
1241            if !same_sense {
1242                segment_points.reverse();
1243            }
1244
1245            // Append to result, avoiding duplicates at connection points
1246            for pt in segment_points {
1247                if all_points.last() != Some(&pt) {
1248                    all_points.push(pt);
1249                }
1250            }
1251        }
1252
1253        Ok(all_points)
1254    }
1255
1256    /// Process composite profile (combination of profiles)
1257    /// IfcCompositeProfileDef: ProfileType, ProfileName, Profiles, Label
1258    fn process_composite(
1259        &self,
1260        profile: &DecodedEntity,
1261        decoder: &mut EntityDecoder,
1262    ) -> Result<Profile2D> {
1263        // Get profiles list (attribute 2)
1264        let profiles_attr = profile
1265            .get(2)
1266            .ok_or_else(|| Error::geometry("Composite profile missing Profiles".to_string()))?;
1267
1268        let sub_profiles = decoder.resolve_ref_list(profiles_attr)?;
1269
1270        if sub_profiles.is_empty() {
1271            return Err(Error::geometry("Composite profile has no sub-profiles".to_string()));
1272        }
1273
1274        // Process first profile as base
1275        let mut result = self.process(&sub_profiles[0], decoder)?;
1276
1277        // Add remaining profiles as holes (simplified - assumes they're holes)
1278        for sub_profile in &sub_profiles[1..] {
1279            let hole = self.process(sub_profile, decoder)?;
1280            result.add_hole(hole.outer);
1281        }
1282
1283        Ok(result)
1284    }
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289    use super::*;
1290
1291    #[test]
1292    fn test_rectangle_profile() {
1293        let content = r#"
1294#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
1295"#;
1296
1297        let mut decoder = EntityDecoder::new(content);
1298        let schema = IfcSchema::new();
1299        let processor = ProfileProcessor::new(schema);
1300
1301        let profile_entity = decoder.decode_by_id(1).unwrap();
1302        let profile = processor.process(&profile_entity, &mut decoder).unwrap();
1303
1304        assert_eq!(profile.outer.len(), 4);
1305        assert!(!profile.outer.is_empty());
1306    }
1307
1308    #[test]
1309    fn test_circle_profile() {
1310        let content = r#"
1311#1=IFCCIRCLEPROFILEDEF(.AREA.,$,$,50.0);
1312"#;
1313
1314        let mut decoder = EntityDecoder::new(content);
1315        let schema = IfcSchema::new();
1316        let processor = ProfileProcessor::new(schema);
1317
1318        let profile_entity = decoder.decode_by_id(1).unwrap();
1319        let profile = processor.process(&profile_entity, &mut decoder).unwrap();
1320
1321        assert_eq!(profile.outer.len(), 24); // Circle with 24 segments
1322        assert!(!profile.outer.is_empty());
1323    }
1324
1325    #[test]
1326    fn test_i_shape_profile() {
1327        let content = r#"
1328#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,200.0,300.0,10.0,15.0,$,$,$,$);
1329"#;
1330
1331        let mut decoder = EntityDecoder::new(content);
1332        let schema = IfcSchema::new();
1333        let processor = ProfileProcessor::new(schema);
1334
1335        let profile_entity = decoder.decode_by_id(1).unwrap();
1336        let profile = processor.process(&profile_entity, &mut decoder).unwrap();
1337
1338        assert_eq!(profile.outer.len(), 12); // I-shape has 12 vertices
1339        assert!(!profile.outer.is_empty());
1340    }
1341
1342    #[test]
1343    fn test_arbitrary_profile() {
1344        let content = r#"
1345#1=IFCCARTESIANPOINT((0.0,0.0));
1346#2=IFCCARTESIANPOINT((100.0,0.0));
1347#3=IFCCARTESIANPOINT((100.0,100.0));
1348#4=IFCCARTESIANPOINT((0.0,100.0));
1349#5=IFCPOLYLINE((#1,#2,#3,#4,#1));
1350#6=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#5);
1351"#;
1352
1353        let mut decoder = EntityDecoder::new(content);
1354        let schema = IfcSchema::new();
1355        let processor = ProfileProcessor::new(schema);
1356
1357        let profile_entity = decoder.decode_by_id(6).unwrap();
1358        let profile = processor.process(&profile_entity, &mut decoder).unwrap();
1359
1360        assert_eq!(profile.outer.len(), 5); // 4 corners + closing point
1361        assert!(!profile.outer.is_empty());
1362    }
1363}