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