1use ifc_lite_core::{
64 build_entity_index, AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcType,
65};
66use serde::{Deserialize, Serialize};
67use std::collections::HashMap;
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct SymbolicPolyline {
77 pub express_id: u32,
79 pub ifc_type: String,
81 pub points: Vec<f32>,
83 pub closed: bool,
85 pub world_y: f32,
88 pub representation: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct SymbolicCircle {
95 pub express_id: u32,
96 pub ifc_type: String,
97 pub center_x: f32,
98 pub center_y: f32,
99 pub radius: f32,
100 pub world_y: f32,
102 pub start_angle: f32,
104 pub end_angle: f32,
106 pub representation: String,
107}
108
109impl SymbolicCircle {
110 pub fn full(
112 express_id: u32,
113 ifc_type: String,
114 center_x: f32,
115 center_y: f32,
116 radius: f32,
117 world_y: f32,
118 representation: String,
119 ) -> Self {
120 Self {
121 express_id,
122 ifc_type,
123 center_x,
124 center_y,
125 radius,
126 world_y,
127 start_angle: 0.0,
128 end_angle: std::f32::consts::TAU,
129 representation,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct SymbolicText {
137 pub express_id: u32,
138 pub ifc_type: String,
139 pub x: f32,
141 pub y: f32,
142 pub dir_x: f32,
144 pub dir_y: f32,
145 pub height: f32,
147 pub content: String,
150 pub alignment: String,
153 pub world_y: f32,
155 pub color: [f32; 4],
158 pub target_px: f32,
161 pub representation: String,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SymbolicFillArea {
171 pub express_id: u32,
172 pub ifc_type: String,
173 pub points: Vec<f32>,
176 pub holes_offsets: Vec<u32>,
178 pub fill_color: [f32; 4],
180 pub has_hatching: bool,
182 pub hatch_spacing: f32,
183 pub hatch_angle: f32,
184 pub hatch_angle_secondary: f32,
186 pub hatch_line_width: f32,
187 pub world_y: f32,
188 pub representation: String,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SymbolicGridAxis {
197 pub express_id: u32,
198 pub grid_express_id: u32,
199 pub tag: String,
200 pub endpoints: [f32; 4],
202 pub world_y: f32,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
207pub struct SymbolicData {
208 pub grid_axes: Vec<SymbolicGridAxis>,
210 pub polylines: Vec<SymbolicPolyline>,
213 pub circles: Vec<SymbolicCircle>,
215 pub texts: Vec<SymbolicText>,
217 pub fills: Vec<SymbolicFillArea>,
219}
220
221impl SymbolicData {
222 pub fn is_empty(&self) -> bool {
226 self.grid_axes.is_empty()
227 && self.polylines.is_empty()
228 && self.circles.is_empty()
229 && self.texts.is_empty()
230 && self.fills.is_empty()
231 }
232}
233
234pub fn extract_symbolic_data<T>(content: &T) -> SymbolicData
245where
246 T: AsRef<[u8]> + ?Sized,
247{
248 let content = content.as_ref();
249 let entity_index = build_entity_index(content);
250 let mut decoder = EntityDecoder::with_index(content, entity_index);
251
252 let router = ifc_lite_geometry::GeometryRouter::with_units(content, &mut decoder);
254 let unit_scale = router.unit_scale() as f32;
255
256 let rtc_offset = router.detect_rtc_offset_from_first_element(content, &mut decoder);
261 let needs_rtc = rtc_offset.0.abs() > 10_000.0
262 || rtc_offset.1.abs() > 10_000.0
263 || rtc_offset.2.abs() > 10_000.0;
264 let rtc_x = if needs_rtc { rtc_offset.0 as f32 } else { 0.0 };
265 let rtc_z = if needs_rtc { rtc_offset.2 as f32 } else { 0.0 };
266
267 let styled_items = build_styled_item_index(content, &mut decoder);
273
274 let mut out = SymbolicData::default();
275 let mut scanner = EntityScanner::new(content);
276
277 while let Some((id, type_name, start, end)) = scanner.next_entity() {
278 let is_grid = type_name == "IFCGRID";
279 if !is_grid && !ifc_lite_core::has_geometry_by_name(type_name) {
280 continue;
284 }
285 let Ok(entity) = decoder.decode_at_with_id(id, start, end) else {
286 continue;
287 };
288
289 if is_grid {
290 let grid_transform = resolve_object_placement(&entity, &mut decoder, unit_scale);
291 extract_grid(
292 &entity,
293 id,
294 &mut decoder,
295 unit_scale,
296 &grid_transform,
297 rtc_x,
298 rtc_z,
299 &mut out,
300 );
301 continue;
302 }
303
304 let Some(representation_attr) = entity.get(6) else {
307 continue;
308 };
309 if representation_attr.is_null() {
310 continue;
311 }
312 let Ok(Some(representation)) = decoder.resolve_ref(representation_attr) else {
313 continue;
314 };
315 let Some(reps_attr) = representation.get(2) else {
316 continue;
317 };
318 let Ok(representations) = decoder.resolve_ref_list(reps_attr) else {
319 continue;
320 };
321
322 let ifc_type_name = entity.ifc_type.name().to_string();
323
324 for shape_rep in representations {
325 if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
326 continue;
327 }
328 let rep_identifier = shape_rep
329 .get(1)
330 .and_then(|a| a.as_string())
331 .unwrap_or("")
332 .to_string();
333 if !matches!(
334 rep_identifier.as_str(),
335 "Plan" | "Annotation" | "FootPrint" | "Axis"
336 ) {
337 continue;
338 }
339
340 let placement_transform = resolve_object_placement(&entity, &mut decoder, unit_scale);
343
344 let context_transform = match shape_rep.get_ref(0) {
347 Some(context_ref) => match decoder.decode_by_id(context_ref) {
348 Ok(context) if context.ifc_type == IfcType::IfcGeometricRepresentationContext => {
349 match context.get_ref(2) {
350 Some(wcs_ref) => match decoder.decode_by_id(wcs_ref) {
351 Ok(wcs) => parse_axis2_placement_2d(&wcs, &mut decoder, unit_scale),
352 Err(_) => Transform2D::identity(),
353 },
354 None => Transform2D::identity(),
355 }
356 }
357 _ => Transform2D::identity(),
360 },
361 None => Transform2D::identity(),
362 };
363 let combined_transform = if context_transform.tx.abs() > 0.001
364 || context_transform.ty.abs() > 0.001
365 || (context_transform.cos_theta - 1.0).abs() > 0.0001
366 || context_transform.sin_theta.abs() > 0.0001
367 {
368 compose_transforms(&context_transform, &placement_transform)
369 } else {
370 placement_transform
371 };
372
373 let Some(items_attr) = shape_rep.get(3) else {
374 continue;
375 };
376 let Ok(items) = decoder.resolve_ref_list(items_attr) else {
377 continue;
378 };
379 for item in items {
380 extract_symbolic_item(
381 &item,
382 &mut decoder,
383 id,
384 &ifc_type_name,
385 &rep_identifier,
386 unit_scale,
387 &combined_transform,
388 rtc_x,
389 rtc_z,
390 &styled_items,
391 &mut out,
392 );
393 }
394 }
395 }
396
397 out
398}
399
400#[derive(Clone, Copy, Debug)]
409struct Transform2D {
410 tx: f32,
411 ty: f32,
412 tz: f32,
413 cos_theta: f32,
414 sin_theta: f32,
415}
416
417impl Transform2D {
418 fn identity() -> Self {
419 Self {
420 tx: 0.0,
421 ty: 0.0,
422 tz: 0.0,
423 cos_theta: 1.0,
424 sin_theta: 0.0,
425 }
426 }
427
428 fn transform_point(&self, x: f32, y: f32) -> (f32, f32) {
429 let rx = x * self.cos_theta - y * self.sin_theta;
430 let ry = x * self.sin_theta + y * self.cos_theta;
431 (rx + self.tx, ry + self.ty)
432 }
433}
434
435fn compose_transforms(a: &Transform2D, b: &Transform2D) -> Transform2D {
437 let combined_cos = a.cos_theta * b.cos_theta - a.sin_theta * b.sin_theta;
438 let combined_sin = a.sin_theta * b.cos_theta + a.cos_theta * b.sin_theta;
439 let rtx = b.tx * a.cos_theta - b.ty * a.sin_theta;
440 let rty = b.tx * a.sin_theta + b.ty * a.cos_theta;
441 Transform2D {
442 tx: rtx + a.tx,
443 ty: rty + a.ty,
444 tz: a.tz + b.tz,
445 cos_theta: combined_cos,
446 sin_theta: combined_sin,
447 }
448}
449
450fn resolve_object_placement(
452 entity: &DecodedEntity,
453 decoder: &mut EntityDecoder,
454 unit_scale: f32,
455) -> Transform2D {
456 let Some(attr) = entity.get(5) else {
457 return Transform2D::identity();
458 };
459 if attr.is_null() {
460 return Transform2D::identity();
461 }
462 let Ok(Some(placement)) = decoder.resolve_ref(attr) else {
463 return Transform2D::identity();
464 };
465 resolve_placement_for_symbolic(&placement, decoder, unit_scale, 0)
466}
467
468fn resolve_placement_for_symbolic(
471 placement: &DecodedEntity,
472 decoder: &mut EntityDecoder,
473 unit_scale: f32,
474 depth: usize,
475) -> Transform2D {
476 if depth > 50 || placement.ifc_type != IfcType::IfcLocalPlacement {
477 return Transform2D::identity();
478 }
479
480 let parent_transform = match placement.get(0) {
481 Some(parent_attr) if !parent_attr.is_null() => match decoder.resolve_ref(parent_attr) {
482 Ok(Some(parent)) => {
483 resolve_placement_for_symbolic(&parent, decoder, unit_scale, depth + 1)
484 }
485 _ => Transform2D::identity(),
486 },
487 _ => Transform2D::identity(),
488 };
489
490 let local_transform = match placement.get(1) {
491 Some(rel_attr) if !rel_attr.is_null() => match decoder.resolve_ref(rel_attr) {
492 Ok(Some(rel))
493 if rel.ifc_type == IfcType::IfcAxis2Placement3D
494 || rel.ifc_type == IfcType::IfcAxis2Placement2D =>
495 {
496 parse_axis2_placement_2d(&rel, decoder, unit_scale)
497 }
498 _ => Transform2D::identity(),
499 },
500 _ => Transform2D::identity(),
501 };
502
503 let combined_cos = parent_transform.cos_theta * local_transform.cos_theta
504 - parent_transform.sin_theta * local_transform.sin_theta;
505 let combined_sin = parent_transform.sin_theta * local_transform.cos_theta
506 + parent_transform.cos_theta * local_transform.sin_theta;
507
508 let rotated_local_tx = local_transform.tx * parent_transform.cos_theta
509 - local_transform.ty * parent_transform.sin_theta;
510 let rotated_local_ty = local_transform.tx * parent_transform.sin_theta
511 + local_transform.ty * parent_transform.cos_theta;
512
513 Transform2D {
514 tx: parent_transform.tx + rotated_local_tx,
515 ty: parent_transform.ty + rotated_local_ty,
516 tz: parent_transform.tz + local_transform.tz,
517 cos_theta: combined_cos,
518 sin_theta: combined_sin,
519 }
520}
521
522fn parse_axis2_placement_2d(
525 placement: &DecodedEntity,
526 decoder: &mut EntityDecoder,
527 unit_scale: f32,
528) -> Transform2D {
529 let is_3d = placement.ifc_type == IfcType::IfcAxis2Placement3D;
530
531 let (tx, ty, tz) = match placement.get_ref(0) {
532 Some(loc_ref) => match decoder.decode_by_id(loc_ref) {
533 Ok(loc) if loc.ifc_type == IfcType::IfcCartesianPoint => {
534 let coords = loc
535 .get(0)
536 .and_then(|a| a.as_list())
537 .map(|l| l.to_vec())
538 .unwrap_or_default();
539 let raw_x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
540 let raw_y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
541 let raw_z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
542 (raw_x * unit_scale, raw_y * unit_scale, raw_z * unit_scale)
543 }
544 _ => (0.0, 0.0, 0.0),
545 },
546 None => (0.0, 0.0, 0.0),
547 };
548
549 let ref_dir_attr = if is_3d {
551 placement.get(2)
552 } else {
553 placement.get(1)
554 };
555 let (cos_theta, sin_theta) = match ref_dir_attr {
556 Some(attr) if !attr.is_null() => match attr.as_entity_ref() {
557 Some(ref_dir_id) => match decoder.decode_by_id(ref_dir_id) {
558 Ok(ref_dir) if ref_dir.ifc_type == IfcType::IfcDirection => {
559 let ratios = ref_dir
560 .get(0)
561 .and_then(|a| a.as_list())
562 .map(|l| l.to_vec())
563 .unwrap_or_default();
564 let dx = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
565 let dy = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
566 let dz = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
567 let len = (dx * dx + dy * dy).sqrt();
568 if len > 0.0001 {
569 (dx / len, dy / len)
570 } else if is_3d && dz.abs() > 0.0001 {
571 (1.0, 0.0)
574 } else {
575 (1.0, 0.0)
576 }
577 }
578 _ => (1.0, 0.0),
579 },
580 None => (1.0, 0.0),
581 },
582 _ => (1.0, 0.0),
583 };
584
585 Transform2D {
586 tx,
587 ty,
588 tz,
589 cos_theta,
590 sin_theta,
591 }
592}
593
594fn parse_cartesian_transformation_operator(
598 operator: &DecodedEntity,
599 decoder: &mut EntityDecoder,
600 unit_scale: f32,
601) -> Transform2D {
602 let (tx, ty) = match operator.get_ref(2) {
604 Some(loc_ref) => match decoder.decode_by_id(loc_ref) {
605 Ok(loc) if loc.ifc_type == IfcType::IfcCartesianPoint => {
606 let coords = loc
607 .get(0)
608 .and_then(|a| a.as_list())
609 .map(|l| l.to_vec())
610 .unwrap_or_default();
611 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
612 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
613 (x * unit_scale, y * unit_scale)
614 }
615 _ => (0.0, 0.0),
616 },
617 None => (0.0, 0.0),
618 };
619
620 let (cos_theta, sin_theta) = match operator.get_ref(0) {
622 Some(ax_ref) => match decoder.decode_by_id(ax_ref) {
623 Ok(ax) if ax.ifc_type == IfcType::IfcDirection => {
624 let ratios = ax
625 .get(0)
626 .and_then(|a| a.as_list())
627 .map(|l| l.to_vec())
628 .unwrap_or_default();
629 let dx = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
630 let dy = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
631 let len = (dx * dx + dy * dy).sqrt();
632 if len > 0.0001 {
633 (dx / len, dy / len)
634 } else {
635 (1.0, 0.0)
636 }
637 }
638 _ => (1.0, 0.0),
639 },
640 None => (1.0, 0.0),
641 };
642
643 Transform2D {
644 tx,
645 ty,
646 tz: 0.0,
647 cos_theta,
648 sin_theta,
649 }
650}
651
652#[allow(clippy::too_many_arguments)]
658fn extract_symbolic_item(
659 item: &DecodedEntity,
660 decoder: &mut EntityDecoder,
661 express_id: u32,
662 ifc_type: &str,
663 rep_identifier: &str,
664 unit_scale: f32,
665 transform: &Transform2D,
666 rtc_x: f32,
667 rtc_z: f32,
668 styled_items: &HashMap<u32, Vec<u32>>,
669 out: &mut SymbolicData,
670) {
671 match item.ifc_type {
672 IfcType::IfcGeometricSet | IfcType::IfcGeometricCurveSet => {
673 if let Some(elements_attr) = item.get(0) {
674 if let Ok(elements) = decoder.resolve_ref_list(elements_attr) {
675 for element in elements {
676 extract_symbolic_item(
677 &element,
678 decoder,
679 express_id,
680 ifc_type,
681 rep_identifier,
682 unit_scale,
683 transform,
684 rtc_x,
685 rtc_z,
686 styled_items,
687 out,
688 );
689 }
690 }
691 }
692 }
693 IfcType::IfcMappedItem => {
694 let Some(source_id) = item.get_ref(0) else { return };
695 let Ok(rep_map) = decoder.decode_by_id(source_id) else { return };
696
697 let mapping_origin_transform = match rep_map.get_ref(0) {
699 Some(origin_id) => match decoder.decode_by_id(origin_id) {
700 Ok(origin) => parse_axis2_placement_2d(&origin, decoder, unit_scale),
701 Err(_) => Transform2D::identity(),
702 },
703 None => Transform2D::identity(),
704 };
705 let mapping_target_transform = match item.get_ref(1) {
707 Some(target_ref) => match decoder.decode_by_id(target_ref) {
708 Ok(target) => parse_cartesian_transformation_operator(&target, decoder, unit_scale),
709 Err(_) => Transform2D::identity(),
710 },
711 None => Transform2D::identity(),
712 };
713 let origin_with_target =
714 compose_transforms(&mapping_target_transform, &mapping_origin_transform);
715 let composed_transform = compose_transforms(transform, &origin_with_target);
716
717 if let Some(mapped_rep_id) = rep_map.get_ref(1) {
718 if let Ok(mapped_rep) = decoder.decode_by_id(mapped_rep_id) {
719 if let Some(items_attr) = mapped_rep.get(3) {
720 if let Ok(items) = decoder.resolve_ref_list(items_attr) {
721 for sub_item in items {
722 extract_symbolic_item(
723 &sub_item,
724 decoder,
725 express_id,
726 ifc_type,
727 rep_identifier,
728 unit_scale,
729 &composed_transform,
730 rtc_x,
731 rtc_z,
732 styled_items,
733 out,
734 );
735 }
736 }
737 }
738 }
739 }
740 }
741 IfcType::IfcPolyline => {
742 if let Some(points_attr) = item.get(0) {
743 if let Ok(point_entities) = decoder.resolve_ref_list(points_attr) {
744 let mut points: Vec<f32> = Vec::with_capacity(point_entities.len() * 2);
745 let mut first_z: Option<f32> = None;
746 for pe in point_entities.iter() {
747 if pe.ifc_type != IfcType::IfcCartesianPoint {
748 continue;
749 }
750 let coords = match pe.get(0).and_then(|a| a.as_list()) {
751 Some(c) => c,
752 None => continue,
753 };
754 let local_x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
755 let local_y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
756 let local_z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
757 if first_z.is_none() {
758 first_z = Some(local_z);
759 }
760 let (wx, wy) = transform.transform_point(local_x, local_y);
761 let x = wx - rtc_x;
762 let y = -wy + rtc_z; if x.is_finite() && y.is_finite() {
764 points.push(x);
765 points.push(y);
766 }
767 }
768 if points.len() >= 4 {
769 let n = points.len();
770 let is_closed = n >= 4
771 && (points[0] - points[n - 2]).abs() < 0.001
772 && (points[1] - points[n - 1]).abs() < 0.001;
773 let world_y = first_z.unwrap_or(0.0) + transform.tz;
774 out.polylines.push(SymbolicPolyline {
775 express_id,
776 ifc_type: ifc_type.to_string(),
777 points,
778 closed: is_closed,
779 world_y,
780 representation: rep_identifier.to_string(),
781 });
782 }
783 }
784 }
785 }
786 IfcType::IfcIndexedPolyCurve => {
787 let Some(points_ref) = item.get_ref(0) else { return };
788 let Ok(points_list) = decoder.decode_by_id(points_ref) else { return };
789 let Some(coord_list_attr) = points_list.get(0) else { return };
790 let Some(coord_list) = coord_list_attr.as_list() else { return };
791 let mut points: Vec<f32> = Vec::with_capacity(coord_list.len() * 2);
792 let mut first_z: Option<f32> = None;
793 for coord in coord_list {
794 let Some(coords) = coord.as_list() else { continue };
795 let local_x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
796 let local_y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
797 let local_z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
798 if first_z.is_none() {
799 first_z = Some(local_z);
800 }
801 let (wx, wy) = transform.transform_point(local_x, local_y);
802 let x = wx - rtc_x;
803 let y = -wy + rtc_z;
804 if x.is_finite() && y.is_finite() {
805 points.push(x);
806 points.push(y);
807 }
808 }
809 if points.len() >= 4 {
810 let n = points.len();
811 let is_closed = n >= 4
812 && (points[0] - points[n - 2]).abs() < 0.001
813 && (points[1] - points[n - 1]).abs() < 0.001;
814 let world_y = first_z.unwrap_or(0.0) + transform.tz;
815 out.polylines.push(SymbolicPolyline {
816 express_id,
817 ifc_type: ifc_type.to_string(),
818 points,
819 closed: is_closed,
820 world_y,
821 representation: rep_identifier.to_string(),
822 });
823 }
824 }
825 IfcType::IfcCircle => {
826 let radius = item.get(1).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
827 if radius <= 0.0 || !radius.is_finite() {
828 return;
829 }
830 let (center_x, center_y, center_z) = circle_center(item, decoder, unit_scale);
831 if !center_x.is_finite() || !center_y.is_finite() {
832 return;
833 }
834 let (wx, wy) = transform.transform_point(center_x, center_y);
835 out.circles.push(SymbolicCircle::full(
836 express_id,
837 ifc_type.to_string(),
838 wx - rtc_x,
839 -wy + rtc_z,
840 radius,
841 center_z + transform.tz,
842 rep_identifier.to_string(),
843 ));
844 }
845 IfcType::IfcEllipse => {
846 let semi_a = item.get(1).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
847 let semi_b = item.get(2).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
848 if semi_a <= 0.0 || semi_b <= 0.0 || !semi_a.is_finite() || !semi_b.is_finite() {
849 return;
850 }
851 let (cx_local, cy_local, cz_local) = circle_center(item, decoder, unit_scale);
852 const SEGMENTS: usize = 64;
853 let mut points: Vec<f32> = Vec::with_capacity((SEGMENTS + 1) * 2);
854 for i in 0..=SEGMENTS {
855 let t = (i as f32) * std::f32::consts::TAU / (SEGMENTS as f32);
856 let lx = cx_local + semi_a * t.cos();
857 let ly = cy_local + semi_b * t.sin();
858 let (wx, wy) = transform.transform_point(lx, ly);
859 let x = wx - rtc_x;
860 let y = -wy + rtc_z;
861 if x.is_finite() && y.is_finite() {
862 points.push(x);
863 points.push(y);
864 }
865 }
866 if points.len() >= 4 {
867 out.polylines.push(SymbolicPolyline {
868 express_id,
869 ifc_type: ifc_type.to_string(),
870 points,
871 closed: true,
872 world_y: cz_local + transform.tz,
873 representation: rep_identifier.to_string(),
874 });
875 }
876 }
877 IfcType::IfcTrimmedCurve => {
878 extract_trimmed_curve(
879 item,
880 decoder,
881 express_id,
882 ifc_type,
883 rep_identifier,
884 unit_scale,
885 transform,
886 rtc_x,
887 rtc_z,
888 out,
889 );
890 }
891 IfcType::IfcCompositeCurve => {
892 if let Some(segments_attr) = item.get(0) {
893 if let Ok(segments) = decoder.resolve_ref_list(segments_attr) {
894 for segment in segments {
895 if let Some(curve_ref) = segment.get_ref(2) {
896 if let Ok(parent_curve) = decoder.decode_by_id(curve_ref) {
897 extract_symbolic_item(
898 &parent_curve,
899 decoder,
900 express_id,
901 ifc_type,
902 rep_identifier,
903 unit_scale,
904 transform,
905 rtc_x,
906 rtc_z,
907 styled_items,
908 out,
909 );
910 }
911 }
912 }
913 }
914 }
915 }
916 IfcType::IfcLine => {
917 }
919 IfcType::IfcTextLiteral | IfcType::IfcTextLiteralWithExtent => {
920 extract_text_literal(
921 item,
922 decoder,
923 express_id,
924 ifc_type,
925 rep_identifier,
926 unit_scale,
927 transform,
928 rtc_x,
929 rtc_z,
930 styled_items,
931 out,
932 );
933 }
934 IfcType::IfcAnnotationFillArea => {
935 extract_annotation_fill_area(
936 item,
937 decoder,
938 express_id,
939 ifc_type,
940 rep_identifier,
941 unit_scale,
942 transform,
943 rtc_x,
944 rtc_z,
945 styled_items,
946 out,
947 );
948 }
949 _ => {
950 }
952 }
953}
954
955fn circle_center(
957 item: &DecodedEntity,
958 decoder: &mut EntityDecoder,
959 unit_scale: f32,
960) -> (f32, f32, f32) {
961 let Some(pos_ref) = item.get_ref(0) else {
962 return (0.0, 0.0, 0.0);
963 };
964 let Ok(placement) = decoder.decode_by_id(pos_ref) else {
965 return (0.0, 0.0, 0.0);
966 };
967 let Some(loc_ref) = placement.get_ref(0) else {
968 return (0.0, 0.0, 0.0);
969 };
970 let Ok(loc) = decoder.decode_by_id(loc_ref) else {
971 return (0.0, 0.0, 0.0);
972 };
973 let Some(coords) = loc.get(0).and_then(|a| a.as_list()) else {
974 return (0.0, 0.0, 0.0);
975 };
976 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
977 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
978 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
979 (x, y, z)
980}
981
982#[allow(clippy::too_many_arguments)]
987fn extract_trimmed_curve(
988 item: &DecodedEntity,
989 decoder: &mut EntityDecoder,
990 express_id: u32,
991 ifc_type: &str,
992 rep_identifier: &str,
993 unit_scale: f32,
994 transform: &Transform2D,
995 rtc_x: f32,
996 rtc_z: f32,
997 out: &mut SymbolicData,
998) {
999 let Some(basis_ref) = item.get_ref(0) else { return };
1000 let Ok(basis_curve) = decoder.decode_by_id(basis_ref) else { return };
1001 if basis_curve.ifc_type != IfcType::IfcCircle {
1002 return;
1003 }
1004 let radius = basis_curve.get(1).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1005 if radius <= 0.0 || !radius.is_finite() {
1006 return;
1007 }
1008 let (center_x, center_y, center_z) = circle_center(&basis_curve, decoder, unit_scale);
1009 if !center_x.is_finite() || !center_y.is_finite() {
1010 return;
1011 }
1012 let world_y = center_z + transform.tz;
1013
1014 let angle_scale = decoder.plane_angle_to_radians() as f32;
1015 let raw_trim1: Option<f32> = item
1016 .get(1)
1017 .and_then(|a| a.as_list().and_then(|l| l.first().and_then(|v| v.as_float())))
1018 .map(|v| v as f32);
1019 let raw_trim2: Option<f32> = item
1020 .get(2)
1021 .and_then(|a| a.as_list().and_then(|l| l.first().and_then(|v| v.as_float())))
1022 .map(|v| v as f32);
1023 let sense = item
1024 .get(3)
1025 .and_then(|v| match v {
1026 AttributeValue::Enum(s) => Some(s == "T" || s == "TRUE" || s == ".T."),
1027 _ => None,
1028 })
1029 .unwrap_or(true);
1030
1031 let start_angle = raw_trim1.map(|v| v * angle_scale).unwrap_or(0.0);
1032 let mut end_angle = raw_trim2.map(|v| v * angle_scale).unwrap_or(std::f32::consts::TAU);
1033 if sense && end_angle < start_angle {
1034 end_angle += std::f32::consts::TAU;
1035 } else if !sense && end_angle > start_angle {
1036 end_angle -= std::f32::consts::TAU;
1037 }
1038 if !start_angle.is_finite() || !end_angle.is_finite() {
1039 return;
1040 }
1041
1042 let start_x = center_x + radius * start_angle.cos();
1043 let start_y = center_y + radius * start_angle.sin();
1044 let end_x = center_x + radius * end_angle.cos();
1045 let end_y = center_y + radius * end_angle.sin();
1046 let chord_dx = end_x - start_x;
1047 let chord_dy = end_y - start_y;
1048 let chord_len = (chord_dx * chord_dx + chord_dy * chord_dy).sqrt();
1049 let is_near_collinear = if chord_len > 0.0001 {
1050 let mid_angle = (start_angle + end_angle) / 2.0;
1051 let mid_x = center_x + radius * mid_angle.cos();
1052 let mid_y = center_y + radius * mid_angle.sin();
1053 let sagitta = ((end_y - start_y) * mid_x - (end_x - start_x) * mid_y
1054 + end_x * start_y
1055 - end_y * start_x)
1056 .abs()
1057 / chord_len;
1058 radius > 100.0 || sagitta < chord_len * 0.02 || radius > chord_len * 10.0
1059 } else {
1060 true
1061 };
1062
1063 if is_near_collinear {
1064 let (wsx, wsy) = transform.transform_point(start_x, start_y);
1065 let (wex, wey) = transform.transform_point(end_x, end_y);
1066 let points = vec![wsx - rtc_x, -wsy + rtc_z, wex - rtc_x, -wey + rtc_z];
1067 out.polylines.push(SymbolicPolyline {
1068 express_id,
1069 ifc_type: ifc_type.to_string(),
1070 points,
1071 closed: false,
1072 world_y,
1073 representation: rep_identifier.to_string(),
1074 });
1075 } else {
1076 let arc_length = (end_angle - start_angle).abs();
1077 let num_segments = ((arc_length * radius / 0.1) as usize).max(8).min(64);
1078 let mut points = Vec::with_capacity((num_segments + 1) * 2);
1079 for i in 0..=num_segments {
1080 let t = i as f32 / num_segments as f32;
1081 let angle = start_angle + t * (end_angle - start_angle);
1082 let local_x = center_x + radius * angle.cos();
1083 let local_y = center_y + radius * angle.sin();
1084 let (wx, wy) = transform.transform_point(local_x, local_y);
1085 let x = wx - rtc_x;
1086 let y = -wy + rtc_z;
1087 if x.is_finite() && y.is_finite() {
1088 points.push(x);
1089 points.push(y);
1090 }
1091 }
1092 if points.len() >= 4 {
1093 out.polylines.push(SymbolicPolyline {
1094 express_id,
1095 ifc_type: ifc_type.to_string(),
1096 points,
1097 closed: false,
1098 world_y,
1099 representation: rep_identifier.to_string(),
1100 });
1101 }
1102 }
1103}
1104
1105#[allow(clippy::too_many_arguments)]
1110fn extract_text_literal(
1111 item: &DecodedEntity,
1112 decoder: &mut EntityDecoder,
1113 express_id: u32,
1114 ifc_type: &str,
1115 rep_identifier: &str,
1116 unit_scale: f32,
1117 transform: &Transform2D,
1118 rtc_x: f32,
1119 rtc_z: f32,
1120 styled_items: &HashMap<u32, Vec<u32>>,
1121 out: &mut SymbolicData,
1122) {
1123 let content = match item.get(0).and_then(|a| a.as_string()) {
1124 Some(s) => s.to_string(),
1125 None => return,
1126 };
1127
1128 let placement_transform = match item.get_ref(1) {
1129 Some(p_ref) => match decoder.decode_by_id(p_ref) {
1130 Ok(p) => parse_axis2_placement_2d(&p, decoder, unit_scale),
1131 Err(_) => Transform2D::identity(),
1132 },
1133 None => Transform2D::identity(),
1134 };
1135 let composed = compose_transforms(transform, &placement_transform);
1136
1137 const CAP_TO_BOX_RATIO: f32 = 0.7;
1138 const FALLBACK_CAP_HEIGHT_M: f32 = 0.18;
1139 let height_model_units = if item.ifc_type == IfcType::IfcTextLiteralWithExtent {
1140 item.get_ref(3)
1141 .and_then(|extent_ref| decoder.decode_by_id(extent_ref).ok())
1142 .and_then(|extent| extent.get(1).and_then(|a| a.as_float()))
1143 .map(|h| (h as f32) * CAP_TO_BOX_RATIO)
1144 .unwrap_or(FALLBACK_CAP_HEIGHT_M / unit_scale.max(1e-6))
1145 } else {
1146 FALLBACK_CAP_HEIGHT_M / unit_scale.max(1e-6)
1147 };
1148
1149 let alignment = if item.ifc_type == IfcType::IfcTextLiteralWithExtent {
1150 item.get(4)
1151 .and_then(|a| a.as_string())
1152 .unwrap_or("")
1153 .to_string()
1154 } else {
1155 String::new()
1156 };
1157
1158 let (wx, wy) = composed.transform_point(0.0, 0.0);
1159 let color = resolve_color_via_styles(item.id, styled_items, decoder)
1160 .unwrap_or([0.05, 0.05, 0.05, 1.0]);
1161
1162 out.texts.push(SymbolicText {
1163 express_id,
1164 ifc_type: ifc_type.to_string(),
1165 x: wx - rtc_x,
1166 y: -wy + rtc_z,
1167 dir_x: composed.cos_theta,
1168 dir_y: -composed.sin_theta, height: height_model_units * unit_scale,
1170 content,
1171 alignment,
1172 world_y: composed.tz,
1173 color,
1174 target_px: 0.0,
1175 representation: rep_identifier.to_string(),
1176 });
1177}
1178
1179#[allow(clippy::too_many_arguments)]
1184fn extract_annotation_fill_area(
1185 item: &DecodedEntity,
1186 decoder: &mut EntityDecoder,
1187 express_id: u32,
1188 ifc_type: &str,
1189 rep_identifier: &str,
1190 unit_scale: f32,
1191 transform: &Transform2D,
1192 rtc_x: f32,
1193 rtc_z: f32,
1194 styled_items: &HashMap<u32, Vec<u32>>,
1195 out: &mut SymbolicData,
1196) {
1197 let Some(outer_ref) = item.get_ref(0) else { return };
1198 let mut points = extract_curve_ring(outer_ref, decoder, unit_scale, transform, rtc_x, rtc_z);
1199 if points.len() < 6 {
1200 return;
1201 }
1202
1203 let mut holes_offsets: Vec<u32> = Vec::new();
1204 if let Some(inners_attr) = item.get(1) {
1205 if let Ok(inner_list) = decoder.resolve_ref_list(inners_attr) {
1206 for inner in inner_list {
1207 let hole = extract_curve_ring(inner.id, decoder, unit_scale, transform, rtc_x, rtc_z);
1208 if hole.len() >= 6 {
1209 let vertex_index = (points.len() / 2) as u32;
1210 holes_offsets.push(vertex_index);
1211 points.extend(hole);
1212 }
1213 }
1214 }
1215 }
1216
1217 let fill_color = resolve_color_via_styles(item.id, styled_items, decoder)
1218 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
1219 let world_y = sample_curve_world_y(outer_ref, decoder, unit_scale) + transform.tz;
1220
1221 out.fills.push(SymbolicFillArea {
1222 express_id,
1223 ifc_type: ifc_type.to_string(),
1224 points,
1225 holes_offsets,
1226 fill_color,
1227 has_hatching: false,
1228 hatch_spacing: 0.0,
1229 hatch_angle: 0.0,
1230 hatch_angle_secondary: f32::NAN,
1231 hatch_line_width: 0.0,
1232 world_y,
1233 representation: rep_identifier.to_string(),
1234 });
1235}
1236
1237fn extract_curve_ring(
1240 curve_id: u32,
1241 decoder: &mut EntityDecoder,
1242 unit_scale: f32,
1243 transform: &Transform2D,
1244 rtc_x: f32,
1245 rtc_z: f32,
1246) -> Vec<f32> {
1247 let Ok(curve) = decoder.decode_by_id(curve_id) else {
1248 return Vec::new();
1249 };
1250 match curve.ifc_type {
1251 IfcType::IfcPolyline => {
1252 let Some(points_attr) = curve.get(0) else { return Vec::new() };
1253 let Ok(point_entities) = decoder.resolve_ref_list(points_attr) else {
1254 return Vec::new();
1255 };
1256 let mut out = Vec::with_capacity(point_entities.len() * 2);
1257 for pe in point_entities {
1258 if pe.ifc_type != IfcType::IfcCartesianPoint {
1259 continue;
1260 }
1261 let Some(coords) = pe.get(0).and_then(|a| a.as_list()) else { continue };
1262 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1263 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1264 let (wx, wy) = transform.transform_point(x, y);
1265 out.push(wx - rtc_x);
1266 out.push(-wy + rtc_z);
1267 }
1268 out
1269 }
1270 IfcType::IfcIndexedPolyCurve => {
1271 let Some(points_ref) = curve.get_ref(0) else { return Vec::new() };
1272 let Ok(points_entity) = decoder.decode_by_id(points_ref) else { return Vec::new() };
1273 let Some(coord_list_attr) = points_entity.get(0) else { return Vec::new() };
1274 let Some(coord_list) = coord_list_attr.as_list() else { return Vec::new() };
1275 let mut out = Vec::with_capacity(coord_list.len() * 2);
1276 for tuple in coord_list {
1277 let Some(coords) = tuple.as_list() else { continue };
1278 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1279 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1280 let (wx, wy) = transform.transform_point(x, y);
1281 out.push(wx - rtc_x);
1282 out.push(-wy + rtc_z);
1283 }
1284 out
1285 }
1286 IfcType::IfcEllipse => {
1287 let semi_a = curve.get(1).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1288 let semi_b = curve.get(2).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1289 if semi_a <= 0.0 || semi_b <= 0.0 || !semi_a.is_finite() || !semi_b.is_finite() {
1290 return Vec::new();
1291 }
1292 let (cx_local, cy_local, _) = circle_center(&curve, decoder, unit_scale);
1293 const SEGMENTS: usize = 64;
1294 let mut out = Vec::with_capacity(SEGMENTS * 2);
1295 for i in 0..SEGMENTS {
1296 let theta = (i as f32) * std::f32::consts::TAU / (SEGMENTS as f32);
1297 let lx = cx_local + semi_a * theta.cos();
1298 let ly = cy_local + semi_b * theta.sin();
1299 let (wx, wy) = transform.transform_point(lx, ly);
1300 out.push(wx - rtc_x);
1301 out.push(-wy + rtc_z);
1302 }
1303 out
1304 }
1305 IfcType::IfcCircle => {
1306 let radius = curve.get(1).and_then(|a| a.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1307 if radius <= 0.0 || !radius.is_finite() {
1308 return Vec::new();
1309 }
1310 let (cx_local, cy_local, _) = circle_center(&curve, decoder, unit_scale);
1311 let seg_count = if radius < 0.05 { 32 } else { 64 };
1312 let mut out = Vec::with_capacity(seg_count * 2);
1313 let two_pi = std::f32::consts::TAU;
1314 for i in 0..seg_count {
1315 let theta = (i as f32) * two_pi / (seg_count as f32);
1316 let lx = cx_local + radius * theta.cos();
1317 let ly = cy_local + radius * theta.sin();
1318 let (wx, wy) = transform.transform_point(lx, ly);
1319 out.push(wx - rtc_x);
1320 out.push(-wy + rtc_z);
1321 }
1322 out
1323 }
1324 _ => Vec::new(),
1325 }
1326}
1327
1328fn sample_curve_world_y(curve_id: u32, decoder: &mut EntityDecoder, unit_scale: f32) -> f32 {
1331 let Ok(curve) = decoder.decode_by_id(curve_id) else { return 0.0 };
1332 match curve.ifc_type {
1333 IfcType::IfcPolyline => {
1334 let Some(points_attr) = curve.get(0) else { return 0.0 };
1335 let Ok(point_entities) = decoder.resolve_ref_list(points_attr) else { return 0.0 };
1336 for pe in point_entities {
1337 if pe.ifc_type != IfcType::IfcCartesianPoint {
1338 continue;
1339 }
1340 if let Some(coords) = pe.get(0).and_then(|a| a.as_list()) {
1341 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1342 return z;
1343 }
1344 }
1345 0.0
1346 }
1347 IfcType::IfcCircle | IfcType::IfcEllipse => {
1348 let (_, _, z) = circle_center(&curve, decoder, unit_scale);
1349 z
1350 }
1351 IfcType::IfcIndexedPolyCurve => {
1352 let Some(points_ref) = curve.get_ref(0) else { return 0.0 };
1353 let Ok(points_entity) = decoder.decode_by_id(points_ref) else { return 0.0 };
1354 let Some(coord_list_attr) = points_entity.get(0) else { return 0.0 };
1355 let Some(coord_list) = coord_list_attr.as_list() else { return 0.0 };
1356 if let Some(first) = coord_list.first().and_then(|v| v.as_list()) {
1357 return first.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32 * unit_scale;
1358 }
1359 0.0
1360 }
1361 _ => 0.0,
1362 }
1363}
1364
1365const BUBBLE_OFFSET_M: f32 = 1.2;
1370const BUBBLE_CAP_M: f32 = 2.0;
1371const BUBBLE_TARGET_PX: f32 = 32.0;
1372const TAG_CAP_M: f32 = 0.7;
1373const TAG_TARGET_PX: f32 = 14.0;
1374
1375#[allow(clippy::too_many_arguments)]
1376fn extract_grid(
1377 grid: &DecodedEntity,
1378 grid_id: u32,
1379 decoder: &mut EntityDecoder,
1380 unit_scale: f32,
1381 transform: &Transform2D,
1382 rtc_x: f32,
1383 rtc_z: f32,
1384 out: &mut SymbolicData,
1385) {
1386 for axis_attr_idx in [7usize, 8, 9] {
1387 let Some(axes_attr) = grid.get(axis_attr_idx) else { continue };
1388 let Ok(axes) = decoder.resolve_ref_list(axes_attr) else { continue };
1389 for axis in axes {
1390 if axis.ifc_type != IfcType::IfcGridAxis {
1391 continue;
1392 }
1393 let axis_id = axis.id;
1394 let tag = axis.get(0).and_then(|a| a.as_string()).unwrap_or("").to_string();
1395
1396 let Some(curve_ref) = axis.get_ref(1) else { continue };
1397 let Ok(curve) = decoder.decode_by_id(curve_ref) else { continue };
1398 let Some((p0, p1)) = sample_grid_axis_endpoints(&curve, decoder, unit_scale, transform)
1399 else {
1400 continue;
1401 };
1402
1403 let a = (p0.0 - rtc_x, -p0.1 + rtc_z);
1404 let b = (p1.0 - rtc_x, -p1.1 + rtc_z);
1405 let world_y = transform.tz;
1406
1407 out.grid_axes.push(SymbolicGridAxis {
1409 express_id: axis_id,
1410 grid_express_id: grid_id,
1411 tag: tag.clone(),
1412 endpoints: [a.0, a.1, b.0, b.1],
1413 world_y,
1414 });
1415
1416 out.polylines.push(SymbolicPolyline {
1418 express_id: axis_id,
1419 ifc_type: "IfcGridAxis".to_string(),
1420 points: vec![a.0, a.1, b.0, b.1],
1421 closed: false,
1422 world_y,
1423 representation: "Axis".to_string(),
1424 });
1425
1426 let dx = b.0 - a.0;
1428 let dy = b.1 - a.1;
1429 let len = (dx * dx + dy * dy).sqrt();
1430 if len < 1e-4 {
1431 continue;
1432 }
1433 let nx = dx / len;
1434 let ny = dy / len;
1435
1436 let cx0 = a.0 - nx * BUBBLE_OFFSET_M;
1437 let cy0 = a.1 - ny * BUBBLE_OFFSET_M;
1438 emit_bubble(axis_id, cx0, cy0, world_y, &tag, out);
1439
1440 let cx1 = b.0 + nx * BUBBLE_OFFSET_M;
1441 let cy1 = b.1 + ny * BUBBLE_OFFSET_M;
1442 emit_bubble(axis_id, cx1, cy1, world_y, &tag, out);
1443 }
1444 }
1445}
1446
1447fn emit_bubble(axis_id: u32, cx: f32, cy: f32, world_y: f32, tag: &str, out: &mut SymbolicData) {
1451 out.texts.push(SymbolicText {
1452 express_id: axis_id,
1453 ifc_type: "IfcGridAxis".to_string(),
1454 x: cx,
1455 y: cy,
1456 dir_x: 1.0,
1457 dir_y: 0.0,
1458 height: BUBBLE_CAP_M,
1459 content: "\u{25EF}".to_string(),
1460 alignment: "center".to_string(),
1461 world_y,
1462 color: [0.0, 0.0, 0.0, 1.0],
1463 target_px: BUBBLE_TARGET_PX,
1464 representation: "Axis".to_string(),
1465 });
1466 out.texts.push(SymbolicText {
1467 express_id: axis_id,
1468 ifc_type: "IfcGridAxis".to_string(),
1469 x: cx,
1470 y: cy,
1471 dir_x: 1.0,
1472 dir_y: 0.0,
1473 height: TAG_CAP_M,
1474 content: tag.to_string(),
1475 alignment: "center".to_string(),
1476 world_y,
1477 color: [0.0, 0.0, 0.0, 1.0],
1478 target_px: TAG_TARGET_PX,
1479 representation: "Axis".to_string(),
1480 });
1481}
1482
1483fn sample_grid_axis_endpoints(
1487 curve: &DecodedEntity,
1488 decoder: &mut EntityDecoder,
1489 unit_scale: f32,
1490 transform: &Transform2D,
1491) -> Option<((f32, f32), (f32, f32))> {
1492 if curve.ifc_type != IfcType::IfcPolyline {
1493 return None;
1494 }
1495 let pts_attr = curve.get(0)?;
1496 let point_entities = decoder.resolve_ref_list(pts_attr).ok()?;
1497 if point_entities.len() < 2 {
1498 return None;
1499 }
1500 let extract = |pe: &DecodedEntity| -> Option<(f32, f32)> {
1501 if pe.ifc_type != IfcType::IfcCartesianPoint {
1502 return None;
1503 }
1504 let coords = pe.get(0)?.as_list()?;
1505 let x = coords.first()?.as_float()? as f32 * unit_scale;
1506 let y = coords.get(1)?.as_float()? as f32 * unit_scale;
1507 Some(transform.transform_point(x, y))
1508 };
1509 let first = extract(&point_entities[0])?;
1510 let last = extract(&point_entities[point_entities.len() - 1])?;
1511 Some((first, last))
1512}
1513
1514fn build_styled_item_index(content: &[u8], decoder: &mut EntityDecoder) -> HashMap<u32, Vec<u32>> {
1519 let collect_refs = |attr: &AttributeValue| -> Vec<u32> {
1520 if let Some(list) = attr.as_list() {
1521 list.iter().filter_map(|v| v.as_entity_ref()).collect()
1522 } else if let Some(single) = attr.as_entity_ref() {
1523 vec![single]
1524 } else {
1525 Vec::new()
1526 }
1527 };
1528
1529 let mut wrappers: HashMap<u32, Vec<u32>> = HashMap::new();
1531 let mut scanner = EntityScanner::new(content);
1532 while let Some((id, type_name, start, end)) = scanner.next_entity() {
1533 if type_name != "IFCPRESENTATIONSTYLEASSIGNMENT" {
1534 continue;
1535 }
1536 let Ok(entity) = decoder.decode_at_with_id(id, start, end) else { continue };
1537 let Some(styles_attr) = entity.get(0) else { continue };
1538 let inner_refs = collect_refs(styles_attr);
1539 if !inner_refs.is_empty() {
1540 wrappers.insert(id, inner_refs);
1541 }
1542 }
1543
1544 let mut out: HashMap<u32, Vec<u32>> = HashMap::new();
1546 let mut scanner = EntityScanner::new(content);
1547 while let Some((id, type_name, start, end)) = scanner.next_entity() {
1548 if type_name != "IFCSTYLEDITEM" {
1549 continue;
1550 }
1551 let Ok(entity) = decoder.decode_at_with_id(id, start, end) else { continue };
1552 let Some(item_ref) = entity.get_ref(0) else { continue };
1553 let Some(styles_attr) = entity.get(1) else { continue };
1554 let mut final_refs: Vec<u32> = Vec::new();
1555 for raw_ref in collect_refs(styles_attr) {
1556 if let Some(inner) = wrappers.get(&raw_ref) {
1557 final_refs.extend(inner.iter().copied());
1558 } else {
1559 final_refs.push(raw_ref);
1560 }
1561 }
1562 if !final_refs.is_empty() {
1563 out.entry(item_ref).or_default().extend(final_refs);
1564 }
1565 }
1566 out
1567}
1568
1569fn resolve_color_via_styles(
1570 item_id: u32,
1571 styled_items: &HashMap<u32, Vec<u32>>,
1572 decoder: &mut EntityDecoder,
1573) -> Option<[f32; 4]> {
1574 let style_refs = styled_items.get(&item_id)?;
1575 for style_ref in style_refs {
1576 if let Some(color) = extract_color_from_style_ref(*style_ref, decoder) {
1577 return Some(color);
1578 }
1579 }
1580 None
1581}
1582
1583fn extract_color_from_style_ref(style_ref: u32, decoder: &mut EntityDecoder) -> Option<[f32; 4]> {
1584 let style = decoder.decode_by_id(style_ref).ok()?;
1585 match style.ifc_type {
1586 IfcType::IfcFillAreaStyle => extract_color_from_fill_area_style(&style, decoder),
1587 IfcType::IfcTextStyle => extract_color_from_text_style(&style, decoder),
1588 _ => None,
1589 }
1590}
1591
1592fn extract_color_from_text_style(
1593 style: &DecodedEntity,
1594 decoder: &mut EntityDecoder,
1595) -> Option<[f32; 4]> {
1596 let appearance = decoder.decode_by_id(style.get_ref(1)?).ok()?;
1597 if appearance.ifc_type != IfcType::IfcTextStyleForDefinedFont {
1598 return None;
1599 }
1600 let colour = decoder.decode_by_id(appearance.get_ref(0)?).ok()?;
1601 if colour.ifc_type != IfcType::IfcColourRgb {
1602 return None;
1603 }
1604 let r = colour.get(1)?.as_float()? as f32;
1605 let g = colour.get(2)?.as_float()? as f32;
1606 let b = colour.get(3)?.as_float()? as f32;
1607 Some([r, g, b, 1.0])
1608}
1609
1610fn extract_color_from_fill_area_style(
1611 style: &DecodedEntity,
1612 decoder: &mut EntityDecoder,
1613) -> Option<[f32; 4]> {
1614 let fill_styles_attr = style.get(1)?;
1615 let fill_style_refs: Vec<u32> = if let Some(list) = fill_styles_attr.as_list() {
1616 list.iter().filter_map(|v| v.as_entity_ref()).collect()
1617 } else if let Some(single) = fill_styles_attr.as_entity_ref() {
1618 vec![single]
1619 } else {
1620 return None;
1621 };
1622 for fs_ref in fill_style_refs {
1623 let Ok(fs) = decoder.decode_by_id(fs_ref) else { continue };
1624 if fs.ifc_type == IfcType::IfcColourRgb {
1625 if let (Some(r), Some(g), Some(b)) = (
1626 fs.get(1).and_then(|v| v.as_float()),
1627 fs.get(2).and_then(|v| v.as_float()),
1628 fs.get(3).and_then(|v| v.as_float()),
1629 ) {
1630 return Some([r as f32, g as f32, b as f32, 1.0]);
1631 }
1632 }
1633 }
1634 None
1635}