1use crate::profiles::ProfileProcessor;
21use crate::{Error, Point3, Result, TessellationQuality, Vector3};
22use ifc_lite_core::{
23 build_entity_index, AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcSchema,
24 IfcType,
25};
26use nalgebra::Matrix4;
27
28#[inline]
40fn is_extruded_area_solid(t: IfcType) -> bool {
41 matches!(t, IfcType::IfcExtrudedAreaSolid)
42}
43
44#[derive(Debug, Clone)]
54pub struct ExtractedProfile {
55 pub express_id: u32,
57 pub ifc_type: String,
59 pub outer_points: Vec<f32>,
61 pub hole_counts: Vec<u32>,
63 pub hole_points: Vec<f32>,
65 pub transform: [f32; 16],
68 pub extrusion_dir: [f32; 3],
70 pub extrusion_depth: f32,
72 pub model_index: u32,
74}
75
76pub fn extract_profiles<T>(content: &T, model_index: u32) -> Vec<ExtractedProfile>
86where
87 T: AsRef<[u8]> + ?Sized,
88{
89 let content = content.as_ref();
90 let entity_index = build_entity_index(content);
91 let mut decoder = EntityDecoder::with_index(content, entity_index);
92
93 let unit_scale = detect_unit_scale(content, &mut decoder);
95
96 let schema = IfcSchema::new();
97 let profile_processor = ProfileProcessor::new(schema);
98
99 let mut results = Vec::new();
100 let mut scanner = EntityScanner::new(content);
101
102 while let Some((id, type_name, start, end)) = scanner.next_entity() {
103 if !ifc_lite_core::has_geometry_by_name(type_name) {
104 continue;
105 }
106
107 let entity = match decoder.decode_at_with_id(id, start, end) {
108 Ok(e) => e,
109 Err(_) => continue,
110 };
111
112 if entity.ifc_type.is_subtype_of(IfcType::IfcFeatureElement) {
120 continue;
121 }
122
123 let element_transform = get_placement_transform(entity.get(5), &mut decoder);
125
126 let elem_tf = scale_translation(element_transform, unit_scale);
128
129 let repr_attr = match entity.get(6) {
131 Some(a) if !a.is_null() => a,
132 _ => continue,
133 };
134 let repr = match decoder.resolve_ref(repr_attr) {
135 Ok(Some(r)) => r,
136 _ => continue,
137 };
138
139 let reprs_attr = match repr.get(2) {
141 Some(a) => a,
142 None => continue,
143 };
144 let representations = match decoder.resolve_ref_list(reprs_attr) {
145 Ok(r) => r,
146 Err(_) => continue,
147 };
148
149 let ifc_type_name = entity.ifc_type.name().to_string();
150
151 for shape_rep in representations {
152 if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
153 continue;
154 }
155
156 let rep_id = shape_rep.get(1).and_then(|a| a.as_string()).unwrap_or("");
158 if rep_id != "Body" && rep_id != "SweptSolid" {
159 continue;
160 }
161
162 let items_attr = match shape_rep.get(3) {
164 Some(a) => a,
165 None => continue,
166 };
167 let items = match decoder.resolve_ref_list(items_attr) {
168 Ok(i) => i,
169 Err(_) => continue,
170 };
171
172 for item in &items {
173 if is_extruded_area_solid(item.ifc_type) {
174 match extract_extruded_solid(
175 id,
176 &ifc_type_name,
177 item,
178 &elem_tf,
179 unit_scale,
180 &profile_processor,
181 &mut decoder,
182 model_index,
183 ) {
184 Ok(entry) => results.push(entry),
185 Err(_e) => {
186 crate::diag::diag_debug!(
187 { element_id = id, ifc_type = %ifc_type_name, error = %_e,
188 "profile_extractor: skipping element" }
189 else {
190 #[cfg(feature = "debug_geometry")]
191 eprintln!("[profile_extractor] Skipping #{id} ({ifc_type_name}): {_e}");
192 }
193 );
194 }
195 }
196 } else if item.ifc_type == IfcType::IfcMappedItem {
197 extract_mapped_item_profiles(
198 id,
199 &ifc_type_name,
200 item,
201 &elem_tf,
202 unit_scale,
203 &profile_processor,
204 &mut decoder,
205 model_index,
206 0,
207 &mut results,
208 );
209 }
210 }
211 }
212 }
213
214 results
215}
216
217const MAX_MAPPED_DEPTH: usize = 3;
223
224fn extract_mapped_item_profiles(
235 element_id: u32,
236 ifc_type: &str,
237 mapped_item: &DecodedEntity,
238 elem_transform: &Matrix4<f64>,
239 unit_scale: f64,
240 profile_processor: &ProfileProcessor,
241 decoder: &mut EntityDecoder,
242 model_index: u32,
243 depth: usize,
244 results: &mut Vec<ExtractedProfile>,
245) {
246 if depth > MAX_MAPPED_DEPTH {
247 crate::diag::diag_debug!(
248 { element_id, ifc_type = %ifc_type, max_depth = MAX_MAPPED_DEPTH,
249 "profile_extractor: max mapped item depth exceeded" }
250 else {
251 #[cfg(feature = "debug_geometry")]
252 eprintln!("[profile_extractor] #{element_id} ({ifc_type}): max mapped item depth exceeded");
253 }
254 );
255 return;
256 }
257
258 let source = match mapped_item
260 .get(0)
261 .and_then(|a| if a.is_null() { None } else { Some(a) })
262 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
263 {
264 Some(s) => s,
265 None => return,
266 };
267
268 let target_tf = match resolve_present_ref(mapped_item.get(1), decoder) {
274 Err(()) => return,
275 Ok(resolved) => match resolved {
276 Some(e) => match parse_cartesian_transformation_operator(&e, decoder) {
277 Ok(m) => m,
278 Err(_) => return,
279 },
280 None => Matrix4::identity(),
281 },
282 };
283
284 let origin_tf = match resolve_present_ref(source.get(0), decoder) {
292 Err(()) => return,
293 Ok(Some(e)) => {
294 let parsed = match e.ifc_type {
295 IfcType::IfcAxis2Placement3D => parse_axis2_placement_3d(&e, decoder).ok(),
296 IfcType::IfcAxis2Placement2D => parse_axis2_placement_2d(&e, decoder).ok(),
297 _ => None,
298 };
299 let Some(m) = parsed else { return };
300 m
301 }
302 Ok(None) => Matrix4::identity(),
303 };
304
305 let scaled_target = scale_translation(target_tf * origin_tf, unit_scale);
307 let composed = elem_transform * scaled_target;
308
309 let mapped_rep = match source
311 .get(1)
312 .and_then(|a| if a.is_null() { None } else { Some(a) })
313 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
314 {
315 Some(r) => r,
316 None => return,
317 };
318
319 let items = match mapped_rep
320 .get(3)
321 .and_then(|a| decoder.resolve_ref_list(a).ok())
322 {
323 Some(i) => i,
324 None => return,
325 };
326
327 for sub_item in &items {
328 if is_extruded_area_solid(sub_item.ifc_type) {
329 match extract_extruded_solid(
330 element_id,
331 ifc_type,
332 sub_item,
333 &composed,
334 unit_scale,
335 profile_processor,
336 decoder,
337 model_index,
338 ) {
339 Ok(entry) => results.push(entry),
340 Err(_e) => {
341 crate::diag::diag_debug!(
342 { element_id, ifc_type = %ifc_type, error = %_e,
343 "profile_extractor: skipping mapped item solid" }
344 else {
345 #[cfg(feature = "debug_geometry")]
346 eprintln!("[profile_extractor] #{element_id} ({ifc_type}) mapped: {_e}");
347 }
348 );
349 }
350 }
351 } else if sub_item.ifc_type == IfcType::IfcMappedItem {
352 extract_mapped_item_profiles(
353 element_id,
354 ifc_type,
355 sub_item,
356 &composed,
357 unit_scale,
358 profile_processor,
359 decoder,
360 model_index,
361 depth + 1,
362 results,
363 );
364 }
365 }
366}
367
368fn parse_cartesian_transformation_operator(
375 entity: &DecodedEntity,
376 decoder: &mut EntityDecoder,
377) -> Result<Matrix4<f64>> {
378 crate::router::transforms::operator::parse_transformation_operator(entity, decoder)
379}
380
381fn extract_extruded_solid(
386 element_id: u32,
387 ifc_type: &str,
388 solid: &DecodedEntity,
389 elem_transform: &Matrix4<f64>,
390 unit_scale: f64,
391 profile_processor: &ProfileProcessor,
392 decoder: &mut EntityDecoder,
393 model_index: u32,
394) -> Result<ExtractedProfile> {
395 let profile_attr = solid
397 .get(0)
398 .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea"))?;
399 let profile_entity = decoder
400 .resolve_ref(profile_attr)?
401 .ok_or_else(|| Error::geometry("Failed to resolve SweptArea"))?;
402 let profile =
405 profile_processor.process(&profile_entity, decoder, TessellationQuality::Medium)?;
406
407 if profile.outer.is_empty() {
408 return Err(Error::geometry("empty profile"));
409 }
410
411 let solid_transform = if let Some(pos_attr) = solid.get(1) {
413 if !pos_attr.is_null() {
414 if let Some(pos_ent) = decoder.resolve_ref(pos_attr)? {
415 if pos_ent.ifc_type == IfcType::IfcAxis2Placement3D {
416 let mut t = parse_axis2_placement_3d(&pos_ent, decoder)?;
417 t[(0, 3)] *= unit_scale;
419 t[(1, 3)] *= unit_scale;
420 t[(2, 3)] *= unit_scale;
421 t
422 } else {
423 Matrix4::identity()
424 }
425 } else {
426 Matrix4::identity()
427 }
428 } else {
429 Matrix4::identity()
430 }
431 } else {
432 Matrix4::identity()
433 };
434
435 let local_dir = parse_extrusion_direction(solid, decoder);
437
438 let raw_depth = solid.get(3).and_then(|v| v.as_float());
441 #[cfg(any(feature = "debug_geometry", feature = "observability"))]
442 if raw_depth.is_none() {
443 crate::diag::diag_debug!(
444 { element_id, ifc_type = %ifc_type,
445 "profile_extractor: missing Depth, defaulting to 1.0" }
446 else {
447 #[cfg(feature = "debug_geometry")]
448 eprintln!(
449 "[profile_extractor] #{element_id} ({ifc_type}): missing Depth, defaulting to 1.0"
450 );
451 }
452 );
453 }
454 let depth = raw_depth.unwrap_or(1.0) * unit_scale;
455
456 let combined_ifc = elem_transform * solid_transform;
458
459 let transform = convert_ifc_to_webgl(&combined_ifc);
461
462 let world_dir_ifc = combined_ifc.transform_vector(&local_dir);
464
465 let extrusion_dir = [
467 world_dir_ifc.x as f32,
468 world_dir_ifc.z as f32, -world_dir_ifc.y as f32, ];
471
472 let outer_points: Vec<f32> = profile
474 .outer
475 .iter()
476 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
477 .collect();
478
479 let hole_counts: Vec<u32> = profile.holes.iter().map(|h| h.len() as u32).collect();
480 let hole_points: Vec<f32> = profile
481 .holes
482 .iter()
483 .flat_map(|h| {
484 h.iter()
485 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
486 })
487 .collect();
488
489 Ok(ExtractedProfile {
490 express_id: element_id,
491 ifc_type: ifc_type.to_string(),
492 outer_points,
493 hole_counts,
494 hole_points,
495 transform,
496 extrusion_dir,
497 extrusion_depth: depth as f32,
498 model_index,
499 })
500}
501
502fn get_placement_transform(
509 placement_attr: Option<&AttributeValue>,
510 decoder: &mut EntityDecoder,
511) -> Matrix4<f64> {
512 let attr = match placement_attr {
513 Some(a) if !a.is_null() => a,
514 _ => return Matrix4::identity(),
515 };
516 match decoder.resolve_ref(attr) {
517 Ok(Some(p)) => get_placement_recursive(&p, decoder, 0),
518 _ => Matrix4::identity(),
519 }
520}
521
522const MAX_PLACEMENT_DEPTH: usize = 100;
523
524fn get_placement_recursive(
525 placement: &DecodedEntity,
526 decoder: &mut EntityDecoder,
527 depth: usize,
528) -> Matrix4<f64> {
529 if depth > MAX_PLACEMENT_DEPTH || placement.ifc_type != IfcType::IfcLocalPlacement {
530 return Matrix4::identity();
531 }
532
533 let parent_tf = if let Some(parent_attr) = placement.get(0) {
535 if !parent_attr.is_null() {
536 match decoder.resolve_ref(parent_attr) {
537 Ok(Some(parent)) => get_placement_recursive(&parent, decoder, depth + 1),
538 _ => Matrix4::identity(),
539 }
540 } else {
541 Matrix4::identity()
542 }
543 } else {
544 Matrix4::identity()
545 };
546
547 let local_tf = if let Some(rel_attr) = placement.get(1) {
549 if !rel_attr.is_null() {
550 match decoder.resolve_ref(rel_attr) {
551 Ok(Some(rel)) if rel.ifc_type == IfcType::IfcAxis2Placement3D => {
552 parse_axis2_placement_3d(&rel, decoder).unwrap_or(Matrix4::identity())
553 }
554 _ => Matrix4::identity(),
555 }
556 } else {
557 Matrix4::identity()
558 }
559 } else {
560 Matrix4::identity()
561 };
562
563 parent_tf * local_tf
564}
565
566fn resolve_present_ref(
579 attr: Option<&AttributeValue>,
580 decoder: &mut EntityDecoder,
581) -> std::result::Result<Option<DecodedEntity>, ()> {
582 match attr {
583 None => Ok(None),
584 Some(a) if a.is_null() => Ok(None),
585 Some(a) => match decoder.resolve_ref(a) {
586 Ok(Some(e)) => Ok(Some(e)),
587 _ => Err(()),
588 },
589 }
590}
591
592fn parse_axis2_placement_2d(
596 placement: &DecodedEntity,
597 decoder: &mut EntityDecoder,
598) -> Result<Matrix4<f64>> {
599 crate::router::transforms::mapped::axis2_placement_2d_matrix(placement, decoder)
600}
601
602fn parse_axis2_placement_3d(
603 placement: &DecodedEntity,
604 decoder: &mut EntityDecoder,
605) -> Result<Matrix4<f64>> {
606 let location =
608 parse_cartesian_point(placement, decoder, 0).unwrap_or(Point3::new(0.0, 0.0, 0.0));
609
610 let z_axis = if let Some(a) = placement.get(1) {
612 if !a.is_null() {
613 decoder
614 .resolve_ref(a)?
615 .map(|e| parse_direction_entity(&e))
616 .transpose()?
617 .unwrap_or(Vector3::new(0.0, 0.0, 1.0))
618 } else {
619 Vector3::new(0.0, 0.0, 1.0)
620 }
621 } else {
622 Vector3::new(0.0, 0.0, 1.0)
623 };
624
625 let x_axis_raw = if let Some(a) = placement.get(2) {
627 if !a.is_null() {
628 decoder
629 .resolve_ref(a)?
630 .map(|e| parse_direction_entity(&e))
631 .transpose()?
632 .unwrap_or(Vector3::new(1.0, 0.0, 0.0))
633 } else {
634 Vector3::new(1.0, 0.0, 0.0)
635 }
636 } else {
637 Vector3::new(1.0, 0.0, 0.0)
638 };
639
640 Ok(crate::transform::build_axis2_matrix(location, z_axis, x_axis_raw))
643}
644
645fn parse_cartesian_point(
647 parent: &DecodedEntity,
648 decoder: &mut EntityDecoder,
649 attr_index: usize,
650) -> Result<Point3<f64>> {
651 let pt_attr = parent
652 .get(attr_index)
653 .ok_or_else(|| Error::geometry("Missing cartesian point attr"))?;
654
655 if pt_attr.is_null() {
656 return Ok(Point3::new(0.0, 0.0, 0.0));
657 }
658
659 let pt_entity = decoder
660 .resolve_ref(pt_attr)?
661 .ok_or_else(|| Error::geometry("Failed to resolve IfcCartesianPoint"))?;
662
663 let coords = pt_entity
664 .get(0)
665 .and_then(|a| a.as_list())
666 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates"))?;
667
668 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
669 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
670 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
671
672 Ok(Point3::new(x, y, z))
673}
674
675fn parse_direction_entity(entity: &DecodedEntity) -> Result<Vector3<f64>> {
677 let ratios = entity
678 .get(0)
679 .and_then(|a| a.as_list())
680 .ok_or_else(|| Error::geometry("IfcDirection missing ratios"))?;
681
682 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
683 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
684 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
685
686 Ok(Vector3::new(x, y, z).normalize())
687}
688
689fn parse_extrusion_direction(solid: &DecodedEntity, decoder: &mut EntityDecoder) -> Vector3<f64> {
691 let default = Vector3::new(0.0, 0.0, 1.0);
692 let dir_attr = match solid.get(2) {
693 Some(a) if !a.is_null() => a,
694 _ => return default,
695 };
696 let dir_ent = match decoder.resolve_ref(dir_attr) {
697 Ok(Some(e)) => e,
698 _ => return default,
699 };
700 let ratios = match dir_ent.get(0).and_then(|a| a.as_list()) {
701 Some(r) => r,
702 None => return default,
703 };
704 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
705 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
706 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
707 let v = Vector3::new(x, y, z);
708 let len = v.norm();
709 if len > 1e-10 {
710 v / len
711 } else {
712 default
713 }
714}
715
716fn scale_translation(mut m: Matrix4<f64>, scale: f64) -> Matrix4<f64> {
722 if scale != 1.0 {
723 m[(0, 3)] *= scale;
724 m[(1, 3)] *= scale;
725 m[(2, 3)] *= scale;
726 }
727 m
728}
729
730fn convert_ifc_to_webgl(m: &Matrix4<f64>) -> [f32; 16] {
735 let mut result = [0.0f32; 16];
736 for col in 0..4 {
737 result[col * 4] = m[(0, col)] as f32; result[col * 4 + 1] = m[(2, col)] as f32; result[col * 4 + 2] = -m[(1, col)] as f32; result[col * 4 + 3] = m[(3, col)] as f32; }
742 result
743}
744
745fn detect_unit_scale(content: &[u8], decoder: &mut EntityDecoder) -> f64 {
747 let mut scanner = EntityScanner::new(content);
748 while let Some((id, type_name, _, _)) = scanner.next_entity() {
749 if type_name == "IFCPROJECT" {
750 if let Ok(scale) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
751 return scale;
752 }
753 break;
754 }
755 }
756 1.0
757}