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 = mapped_item
270 .get(1)
271 .and_then(|a| if a.is_null() { None } else { Some(a) })
272 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
273 .and_then(|e| parse_cartesian_transformation_operator(&e, decoder).ok())
274 .unwrap_or_else(Matrix4::identity);
275
276 let scaled_target = scale_translation(target_tf, unit_scale);
278 let composed = elem_transform * scaled_target;
279
280 let mapped_rep = match source
282 .get(1)
283 .and_then(|a| if a.is_null() { None } else { Some(a) })
284 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
285 {
286 Some(r) => r,
287 None => return,
288 };
289
290 let items = match mapped_rep
291 .get(3)
292 .and_then(|a| decoder.resolve_ref_list(a).ok())
293 {
294 Some(i) => i,
295 None => return,
296 };
297
298 for sub_item in &items {
299 if is_extruded_area_solid(sub_item.ifc_type) {
300 match extract_extruded_solid(
301 element_id,
302 ifc_type,
303 sub_item,
304 &composed,
305 unit_scale,
306 profile_processor,
307 decoder,
308 model_index,
309 ) {
310 Ok(entry) => results.push(entry),
311 Err(_e) => {
312 crate::diag::diag_debug!(
313 { element_id, ifc_type = %ifc_type, error = %_e,
314 "profile_extractor: skipping mapped item solid" }
315 else {
316 #[cfg(feature = "debug_geometry")]
317 eprintln!("[profile_extractor] #{element_id} ({ifc_type}) mapped: {_e}");
318 }
319 );
320 }
321 }
322 } else if sub_item.ifc_type == IfcType::IfcMappedItem {
323 extract_mapped_item_profiles(
324 element_id,
325 ifc_type,
326 sub_item,
327 &composed,
328 unit_scale,
329 profile_processor,
330 decoder,
331 model_index,
332 depth + 1,
333 results,
334 );
335 }
336 }
337}
338
339fn parse_cartesian_transformation_operator(
348 entity: &DecodedEntity,
349 decoder: &mut EntityDecoder,
350) -> Result<Matrix4<f64>> {
351 let origin = parse_cartesian_point(entity, decoder, 2).unwrap_or(Point3::new(0.0, 0.0, 0.0));
353
354 let scale = entity.get(3).and_then(|v| v.as_float()).unwrap_or(1.0);
356
357 let x_axis = entity
359 .get(0)
360 .filter(|a| !a.is_null())
361 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
362 .and_then(|e| parse_direction_entity(&e).ok())
363 .unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0))
364 .normalize();
365
366 let z_axis = entity
368 .get(4)
369 .filter(|a| !a.is_null())
370 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
371 .and_then(|e| parse_direction_entity(&e).ok())
372 .unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0))
373 .normalize();
374
375 let y_axis = z_axis.cross(&x_axis).normalize();
377 let x_axis = y_axis.cross(&z_axis).normalize();
378
379 #[rustfmt::skip]
380 let m = Matrix4::new(
381 x_axis.x * scale, y_axis.x * scale, z_axis.x * scale, origin.x,
382 x_axis.y * scale, y_axis.y * scale, z_axis.y * scale, origin.y,
383 x_axis.z * scale, y_axis.z * scale, z_axis.z * scale, origin.z,
384 0.0, 0.0, 0.0, 1.0,
385 );
386 Ok(m)
387}
388
389fn extract_extruded_solid(
394 element_id: u32,
395 ifc_type: &str,
396 solid: &DecodedEntity,
397 elem_transform: &Matrix4<f64>,
398 unit_scale: f64,
399 profile_processor: &ProfileProcessor,
400 decoder: &mut EntityDecoder,
401 model_index: u32,
402) -> Result<ExtractedProfile> {
403 let profile_attr = solid
405 .get(0)
406 .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea"))?;
407 let profile_entity = decoder
408 .resolve_ref(profile_attr)?
409 .ok_or_else(|| Error::geometry("Failed to resolve SweptArea"))?;
410 let profile =
413 profile_processor.process(&profile_entity, decoder, TessellationQuality::Medium)?;
414
415 if profile.outer.is_empty() {
416 return Err(Error::geometry("empty profile"));
417 }
418
419 let solid_transform = if let Some(pos_attr) = solid.get(1) {
421 if !pos_attr.is_null() {
422 if let Some(pos_ent) = decoder.resolve_ref(pos_attr)? {
423 if pos_ent.ifc_type == IfcType::IfcAxis2Placement3D {
424 let mut t = parse_axis2_placement_3d(&pos_ent, decoder)?;
425 t[(0, 3)] *= unit_scale;
427 t[(1, 3)] *= unit_scale;
428 t[(2, 3)] *= unit_scale;
429 t
430 } else {
431 Matrix4::identity()
432 }
433 } else {
434 Matrix4::identity()
435 }
436 } else {
437 Matrix4::identity()
438 }
439 } else {
440 Matrix4::identity()
441 };
442
443 let local_dir = parse_extrusion_direction(solid, decoder);
445
446 let raw_depth = solid.get(3).and_then(|v| v.as_float());
449 #[cfg(any(feature = "debug_geometry", feature = "observability"))]
450 if raw_depth.is_none() {
451 crate::diag::diag_debug!(
452 { element_id, ifc_type = %ifc_type,
453 "profile_extractor: missing Depth, defaulting to 1.0" }
454 else {
455 #[cfg(feature = "debug_geometry")]
456 eprintln!(
457 "[profile_extractor] #{element_id} ({ifc_type}): missing Depth, defaulting to 1.0"
458 );
459 }
460 );
461 }
462 let depth = raw_depth.unwrap_or(1.0) * unit_scale;
463
464 let combined_ifc = elem_transform * solid_transform;
466
467 let transform = convert_ifc_to_webgl(&combined_ifc);
469
470 let world_dir_ifc = combined_ifc.transform_vector(&local_dir);
472
473 let extrusion_dir = [
475 world_dir_ifc.x as f32,
476 world_dir_ifc.z as f32, -world_dir_ifc.y as f32, ];
479
480 let outer_points: Vec<f32> = profile
482 .outer
483 .iter()
484 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
485 .collect();
486
487 let hole_counts: Vec<u32> = profile.holes.iter().map(|h| h.len() as u32).collect();
488 let hole_points: Vec<f32> = profile
489 .holes
490 .iter()
491 .flat_map(|h| {
492 h.iter()
493 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
494 })
495 .collect();
496
497 Ok(ExtractedProfile {
498 express_id: element_id,
499 ifc_type: ifc_type.to_string(),
500 outer_points,
501 hole_counts,
502 hole_points,
503 transform,
504 extrusion_dir,
505 extrusion_depth: depth as f32,
506 model_index,
507 })
508}
509
510fn get_placement_transform(
517 placement_attr: Option<&AttributeValue>,
518 decoder: &mut EntityDecoder,
519) -> Matrix4<f64> {
520 let attr = match placement_attr {
521 Some(a) if !a.is_null() => a,
522 _ => return Matrix4::identity(),
523 };
524 match decoder.resolve_ref(attr) {
525 Ok(Some(p)) => get_placement_recursive(&p, decoder, 0),
526 _ => Matrix4::identity(),
527 }
528}
529
530const MAX_PLACEMENT_DEPTH: usize = 100;
531
532fn get_placement_recursive(
533 placement: &DecodedEntity,
534 decoder: &mut EntityDecoder,
535 depth: usize,
536) -> Matrix4<f64> {
537 if depth > MAX_PLACEMENT_DEPTH || placement.ifc_type != IfcType::IfcLocalPlacement {
538 return Matrix4::identity();
539 }
540
541 let parent_tf = if let Some(parent_attr) = placement.get(0) {
543 if !parent_attr.is_null() {
544 match decoder.resolve_ref(parent_attr) {
545 Ok(Some(parent)) => get_placement_recursive(&parent, decoder, depth + 1),
546 _ => Matrix4::identity(),
547 }
548 } else {
549 Matrix4::identity()
550 }
551 } else {
552 Matrix4::identity()
553 };
554
555 let local_tf = if let Some(rel_attr) = placement.get(1) {
557 if !rel_attr.is_null() {
558 match decoder.resolve_ref(rel_attr) {
559 Ok(Some(rel)) if rel.ifc_type == IfcType::IfcAxis2Placement3D => {
560 parse_axis2_placement_3d(&rel, decoder).unwrap_or(Matrix4::identity())
561 }
562 _ => Matrix4::identity(),
563 }
564 } else {
565 Matrix4::identity()
566 }
567 } else {
568 Matrix4::identity()
569 };
570
571 parent_tf * local_tf
572}
573
574fn parse_axis2_placement_3d(
581 placement: &DecodedEntity,
582 decoder: &mut EntityDecoder,
583) -> Result<Matrix4<f64>> {
584 let location =
586 parse_cartesian_point(placement, decoder, 0).unwrap_or(Point3::new(0.0, 0.0, 0.0));
587
588 let z_axis = if let Some(a) = placement.get(1) {
590 if !a.is_null() {
591 decoder
592 .resolve_ref(a)?
593 .map(|e| parse_direction_entity(&e))
594 .transpose()?
595 .unwrap_or(Vector3::new(0.0, 0.0, 1.0))
596 } else {
597 Vector3::new(0.0, 0.0, 1.0)
598 }
599 } else {
600 Vector3::new(0.0, 0.0, 1.0)
601 };
602
603 let x_axis_raw = if let Some(a) = placement.get(2) {
605 if !a.is_null() {
606 decoder
607 .resolve_ref(a)?
608 .map(|e| parse_direction_entity(&e))
609 .transpose()?
610 .unwrap_or(Vector3::new(1.0, 0.0, 0.0))
611 } else {
612 Vector3::new(1.0, 0.0, 0.0)
613 }
614 } else {
615 Vector3::new(1.0, 0.0, 0.0)
616 };
617
618 Ok(crate::transform::build_axis2_matrix(location, z_axis, x_axis_raw))
621}
622
623fn parse_cartesian_point(
625 parent: &DecodedEntity,
626 decoder: &mut EntityDecoder,
627 attr_index: usize,
628) -> Result<Point3<f64>> {
629 let pt_attr = parent
630 .get(attr_index)
631 .ok_or_else(|| Error::geometry("Missing cartesian point attr"))?;
632
633 if pt_attr.is_null() {
634 return Ok(Point3::new(0.0, 0.0, 0.0));
635 }
636
637 let pt_entity = decoder
638 .resolve_ref(pt_attr)?
639 .ok_or_else(|| Error::geometry("Failed to resolve IfcCartesianPoint"))?;
640
641 let coords = pt_entity
642 .get(0)
643 .and_then(|a| a.as_list())
644 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates"))?;
645
646 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
647 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
648 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
649
650 Ok(Point3::new(x, y, z))
651}
652
653fn parse_direction_entity(entity: &DecodedEntity) -> Result<Vector3<f64>> {
655 let ratios = entity
656 .get(0)
657 .and_then(|a| a.as_list())
658 .ok_or_else(|| Error::geometry("IfcDirection missing ratios"))?;
659
660 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
661 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
662 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
663
664 Ok(Vector3::new(x, y, z).normalize())
665}
666
667fn parse_extrusion_direction(solid: &DecodedEntity, decoder: &mut EntityDecoder) -> Vector3<f64> {
669 let default = Vector3::new(0.0, 0.0, 1.0);
670 let dir_attr = match solid.get(2) {
671 Some(a) if !a.is_null() => a,
672 _ => return default,
673 };
674 let dir_ent = match decoder.resolve_ref(dir_attr) {
675 Ok(Some(e)) => e,
676 _ => return default,
677 };
678 let ratios = match dir_ent.get(0).and_then(|a| a.as_list()) {
679 Some(r) => r,
680 None => return default,
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 let v = Vector3::new(x, y, z);
686 let len = v.norm();
687 if len > 1e-10 {
688 v / len
689 } else {
690 default
691 }
692}
693
694fn scale_translation(mut m: Matrix4<f64>, scale: f64) -> Matrix4<f64> {
700 if scale != 1.0 {
701 m[(0, 3)] *= scale;
702 m[(1, 3)] *= scale;
703 m[(2, 3)] *= scale;
704 }
705 m
706}
707
708fn convert_ifc_to_webgl(m: &Matrix4<f64>) -> [f32; 16] {
713 let mut result = [0.0f32; 16];
714 for col in 0..4 {
715 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; }
720 result
721}
722
723fn detect_unit_scale(content: &[u8], decoder: &mut EntityDecoder) -> f64 {
725 let mut scanner = EntityScanner::new(content);
726 while let Some((id, type_name, _, _)) = scanner.next_entity() {
727 if type_name == "IFCPROJECT" {
728 if let Ok(scale) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
729 return scale;
730 }
731 break;
732 }
733 }
734 1.0
735}