Skip to main content

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