1use 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
14pub struct ProfileProcessor {
16 schema: IfcSchema,
17}
18
19impl ProfileProcessor {
20 pub fn new(schema: IfcSchema) -> Self {
22 Self { schema }
23 }
24
25 #[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 #[inline]
45 fn process_parametric(
46 &self,
47 profile: &DecodedEntity,
48 decoder: &mut EntityDecoder,
49 ) -> Result<Profile2D> {
50 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 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 fn apply_profile_position(
85 &self,
86 profile: &mut Profile2D,
87 placement: &DecodedEntity,
88 decoder: &mut EntityDecoder,
89 ) -> Result<()> {
90 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 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 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 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 let x_axis = (dir_x, dir_y);
145 let y_axis = (-dir_y, dir_x);
146
147 for point in &mut profile.outer {
149 let old_x = point.x;
150 let old_y = point.y;
151 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 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 #[inline]
172 fn process_rectangle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
173 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 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 #[inline]
198 fn process_circle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
199 let radius = profile
201 .get_float(3)
202 .ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
203
204 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 fn process_i_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
221 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 let points = vec![
241 Point2::new(-half_width, -half_depth),
243 Point2::new(half_width, -half_depth),
244 Point2::new(half_width, -half_depth + flange_thickness),
245 Point2::new(half_web, -half_depth + flange_thickness),
247 Point2::new(half_web, half_depth - flange_thickness),
248 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); let half_depth = depth / 2.0;
422
423 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 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 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 fn process_arbitrary(
472 &self,
473 profile: &DecodedEntity,
474 decoder: &mut EntityDecoder,
475 ) -> Result<Profile2D> {
476 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 let outer_points = self.process_curve(&curve, decoder)?;
487 let mut result = Profile2D::new(outer_points);
488
489 if profile.ifc_type == IfcType::IfcArbitraryProfileDefWithVoids {
491 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 #[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 #[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 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 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 fn process_circle_3d(
551 &self,
552 curve: &DecodedEntity,
553 decoder: &mut EntityDecoder,
554 ) -> Result<Vec<Point3<f64>>> {
555 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 let (center, x_axis, y_axis) = if position.ifc_type == IfcType::IfcAxis2Placement3D {
570 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 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 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 let y_axis = z_axis.cross(&x_axis).normalize();
638
639 (center, x_axis, y_axis)
640 } else {
641 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 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 fn process_polyline_3d(
683 &self,
684 curve: &DecodedEntity,
685 decoder: &mut EntityDecoder,
686 ) -> Result<Vec<Point3<f64>>> {
687 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 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 fn process_composite_curve_3d(
717 &self,
718 curve: &DecodedEntity,
719 decoder: &mut EntityDecoder,
720 ) -> Result<Vec<Point3<f64>>> {
721 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 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 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 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 fn process_trimmed_curve(
768 &self,
769 curve: &DecodedEntity,
770 decoder: &mut EntityDecoder,
771 ) -> Result<Vec<Point2<f64>>> {
772 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 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 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 match basis_curve.ifc_type {
796 IfcType::IfcCircle | IfcType::IfcEllipse => {
797 self.process_trimmed_conic(&basis_curve, trim1, trim2, sense, decoder)
798 }
799 _ => {
800 self.process_curve(&basis_curve, decoder)
802 }
803 }
804 }
805
806 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 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 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 let start_angle = trim1.unwrap_or(0.0).to_radians();
848 let end_angle = trim2.unwrap_or(360.0).to_radians();
849
850 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 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 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 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 #[inline]
992 fn process_polyline(
993 &self,
994 polyline: &DecodedEntity,
995 decoder: &mut EntityDecoder,
996 ) -> Result<Vec<Point2<f64>>> {
997 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 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 fn process_indexed_polycurve(
1031 &self,
1032 curve: &DecodedEntity,
1033 decoder: &mut EntityDecoder,
1034 ) -> Result<Vec<Point2<f64>>> {
1035 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 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 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 let segments_attr = curve.get(1);
1067
1068 if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
1069 return Ok(all_points);
1071 }
1072
1073 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 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)) .collect();
1089
1090 if idx_values.len() == 3 {
1091 let p1 = all_points.get(idx_values[0]).copied();
1093 let p2 = all_points.get(idx_values[1]).copied(); let p3 = all_points.get(idx_values[2]).copied();
1095
1096 if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
1097 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 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 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 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 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 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 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 let mut start_angle = angle1;
1169 let mut end_angle = angle3;
1170
1171 let mid_check = angle1 + (angle3 - angle1) / 2.0;
1173 let diff = (angle2 - mid_check).abs();
1174 if diff > PI {
1175 if end_angle > start_angle {
1177 end_angle -= 2.0 * PI;
1178 } else {
1179 end_angle += 2.0 * PI;
1180 }
1181 }
1182
1183 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 fn process_composite_curve(
1200 &self,
1201 curve: &DecodedEntity,
1202 decoder: &mut EntityDecoder,
1203 ) -> Result<Vec<Point2<f64>>> {
1204 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 if segment.ifc_type != IfcType::IfcCompositeCurveSegment {
1216 continue;
1217 }
1218
1219 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 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 let mut segment_points = self.process_curve(&parent_curve, decoder)?;
1240
1241 if !same_sense {
1242 segment_points.reverse();
1243 }
1244
1245 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 fn process_composite(
1259 &self,
1260 profile: &DecodedEntity,
1261 decoder: &mut EntityDecoder,
1262 ) -> Result<Profile2D> {
1263 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 let mut result = self.process(&sub_profiles[0], decoder)?;
1276
1277 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); 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); 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); assert!(!profile.outer.is_empty());
1362 }
1363}