1use crate::profiles::ProfileProcessor;
21use crate::{Error, Point3, Result, Vector3};
22use ifc_lite_core::{
23 build_entity_index, AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcSchema,
24 IfcType,
25};
26use nalgebra::Matrix4;
27
28#[derive(Debug, Clone)]
38pub struct ExtractedProfile {
39 pub express_id: u32,
41 pub ifc_type: String,
43 pub outer_points: Vec<f32>,
45 pub hole_counts: Vec<u32>,
47 pub hole_points: Vec<f32>,
49 pub transform: [f32; 16],
52 pub extrusion_dir: [f32; 3],
54 pub extrusion_depth: f32,
56 pub model_index: u32,
58}
59
60pub fn extract_profiles(content: &str, model_index: u32) -> Vec<ExtractedProfile> {
70 let entity_index = build_entity_index(content);
71 let mut decoder = EntityDecoder::with_index(content, entity_index);
72
73 let unit_scale = detect_unit_scale(content, &mut decoder);
75
76 let schema = IfcSchema::new();
77 let profile_processor = ProfileProcessor::new(schema);
78
79 let mut results = Vec::new();
80 let mut scanner = EntityScanner::new(content);
81
82 while let Some((id, type_name, start, end)) = scanner.next_entity() {
83 if !ifc_lite_core::has_geometry_by_name(type_name) {
84 continue;
85 }
86
87 let entity = match decoder.decode_at_with_id(id, start, end) {
88 Ok(e) => e,
89 Err(_) => continue,
90 };
91
92 let element_transform = get_placement_transform(entity.get(5), &mut decoder);
94
95 let elem_tf = scale_translation(element_transform, unit_scale);
97
98 let repr_attr = match entity.get(6) {
100 Some(a) if !a.is_null() => a,
101 _ => continue,
102 };
103 let repr = match decoder.resolve_ref(repr_attr) {
104 Ok(Some(r)) => r,
105 _ => continue,
106 };
107
108 let reprs_attr = match repr.get(2) {
110 Some(a) => a,
111 None => continue,
112 };
113 let representations = match decoder.resolve_ref_list(reprs_attr) {
114 Ok(r) => r,
115 Err(_) => continue,
116 };
117
118 let ifc_type_name = entity.ifc_type.name().to_string();
119
120 for shape_rep in representations {
121 if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
122 continue;
123 }
124
125 let rep_id = shape_rep.get(1).and_then(|a| a.as_string()).unwrap_or("");
127 if rep_id != "Body" && rep_id != "SweptSolid" {
128 continue;
129 }
130
131 let items_attr = match shape_rep.get(3) {
133 Some(a) => a,
134 None => continue,
135 };
136 let items = match decoder.resolve_ref_list(items_attr) {
137 Ok(i) => i,
138 Err(_) => continue,
139 };
140
141 for item in &items {
142 if item.ifc_type == IfcType::IfcExtrudedAreaSolid {
143 match extract_extruded_solid(
144 id,
145 &ifc_type_name,
146 item,
147 &elem_tf,
148 unit_scale,
149 &profile_processor,
150 &mut decoder,
151 model_index,
152 ) {
153 Ok(entry) => results.push(entry),
154 Err(_e) => {
155 #[cfg(feature = "debug_geometry")]
156 eprintln!("[profile_extractor] Skipping #{id} ({ifc_type_name}): {_e}");
157 }
158 }
159 } else if item.ifc_type == IfcType::IfcMappedItem {
160 extract_mapped_item_profiles(
161 id,
162 &ifc_type_name,
163 item,
164 &elem_tf,
165 unit_scale,
166 &profile_processor,
167 &mut decoder,
168 model_index,
169 0,
170 &mut results,
171 );
172 }
173 }
174 }
175 }
176
177 results
178}
179
180const MAX_MAPPED_DEPTH: usize = 3;
186
187fn extract_mapped_item_profiles(
198 element_id: u32,
199 ifc_type: &str,
200 mapped_item: &DecodedEntity,
201 elem_transform: &Matrix4<f64>,
202 unit_scale: f64,
203 profile_processor: &ProfileProcessor,
204 decoder: &mut EntityDecoder,
205 model_index: u32,
206 depth: usize,
207 results: &mut Vec<ExtractedProfile>,
208) {
209 if depth > MAX_MAPPED_DEPTH {
210 #[cfg(feature = "debug_geometry")]
211 eprintln!("[profile_extractor] #{element_id} ({ifc_type}): max mapped item depth exceeded");
212 return;
213 }
214
215 let source = match mapped_item
217 .get(0)
218 .and_then(|a| if a.is_null() { None } else { Some(a) })
219 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
220 {
221 Some(s) => s,
222 None => return,
223 };
224
225 let target_tf = mapped_item
227 .get(1)
228 .and_then(|a| if a.is_null() { None } else { Some(a) })
229 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
230 .and_then(|e| parse_cartesian_transformation_operator(&e, decoder).ok())
231 .unwrap_or_else(Matrix4::identity);
232
233 let scaled_target = scale_translation(target_tf, unit_scale);
235 let composed = elem_transform * scaled_target;
236
237 let mapped_rep = match source
239 .get(1)
240 .and_then(|a| if a.is_null() { None } else { Some(a) })
241 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
242 {
243 Some(r) => r,
244 None => return,
245 };
246
247 let items = match mapped_rep
248 .get(3)
249 .and_then(|a| decoder.resolve_ref_list(a).ok())
250 {
251 Some(i) => i,
252 None => return,
253 };
254
255 for sub_item in &items {
256 if sub_item.ifc_type == IfcType::IfcExtrudedAreaSolid {
257 match extract_extruded_solid(
258 element_id,
259 ifc_type,
260 sub_item,
261 &composed,
262 unit_scale,
263 profile_processor,
264 decoder,
265 model_index,
266 ) {
267 Ok(entry) => results.push(entry),
268 Err(_e) => {
269 #[cfg(feature = "debug_geometry")]
270 eprintln!("[profile_extractor] #{element_id} ({ifc_type}) mapped: {_e}");
271 }
272 }
273 } else if sub_item.ifc_type == IfcType::IfcMappedItem {
274 extract_mapped_item_profiles(
275 element_id,
276 ifc_type,
277 sub_item,
278 &composed,
279 unit_scale,
280 profile_processor,
281 decoder,
282 model_index,
283 depth + 1,
284 results,
285 );
286 }
287 }
288}
289
290fn parse_cartesian_transformation_operator(
299 entity: &DecodedEntity,
300 decoder: &mut EntityDecoder,
301) -> Result<Matrix4<f64>> {
302 let origin = parse_cartesian_point(entity, decoder, 2).unwrap_or(Point3::new(0.0, 0.0, 0.0));
304
305 let scale = entity.get(3).and_then(|v| v.as_float()).unwrap_or(1.0);
307
308 let x_axis = entity
310 .get(0)
311 .filter(|a| !a.is_null())
312 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
313 .and_then(|e| parse_direction_entity(&e).ok())
314 .unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0))
315 .normalize();
316
317 let z_axis = entity
319 .get(4)
320 .filter(|a| !a.is_null())
321 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
322 .and_then(|e| parse_direction_entity(&e).ok())
323 .unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0))
324 .normalize();
325
326 let y_axis = z_axis.cross(&x_axis).normalize();
328 let x_axis = y_axis.cross(&z_axis).normalize();
329
330 #[rustfmt::skip]
331 let m = Matrix4::new(
332 x_axis.x * scale, y_axis.x * scale, z_axis.x * scale, origin.x,
333 x_axis.y * scale, y_axis.y * scale, z_axis.y * scale, origin.y,
334 x_axis.z * scale, y_axis.z * scale, z_axis.z * scale, origin.z,
335 0.0, 0.0, 0.0, 1.0,
336 );
337 Ok(m)
338}
339
340fn extract_extruded_solid(
345 element_id: u32,
346 ifc_type: &str,
347 solid: &DecodedEntity,
348 elem_transform: &Matrix4<f64>,
349 unit_scale: f64,
350 profile_processor: &ProfileProcessor,
351 decoder: &mut EntityDecoder,
352 model_index: u32,
353) -> Result<ExtractedProfile> {
354 let profile_attr = solid
356 .get(0)
357 .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea"))?;
358 let profile_entity = decoder
359 .resolve_ref(profile_attr)?
360 .ok_or_else(|| Error::geometry("Failed to resolve SweptArea"))?;
361 let profile = profile_processor.process(&profile_entity, decoder)?;
362
363 if profile.outer.is_empty() {
364 return Err(Error::geometry("empty profile"));
365 }
366
367 let solid_transform = if let Some(pos_attr) = solid.get(1) {
369 if !pos_attr.is_null() {
370 if let Some(pos_ent) = decoder.resolve_ref(pos_attr)? {
371 if pos_ent.ifc_type == IfcType::IfcAxis2Placement3D {
372 let mut t = parse_axis2_placement_3d(&pos_ent, decoder)?;
373 t[(0, 3)] *= unit_scale;
375 t[(1, 3)] *= unit_scale;
376 t[(2, 3)] *= unit_scale;
377 t
378 } else {
379 Matrix4::identity()
380 }
381 } else {
382 Matrix4::identity()
383 }
384 } else {
385 Matrix4::identity()
386 }
387 } else {
388 Matrix4::identity()
389 };
390
391 let local_dir = parse_extrusion_direction(solid, decoder);
393
394 let raw_depth = solid.get(3).and_then(|v| v.as_float());
397 #[cfg(feature = "debug_geometry")]
398 if raw_depth.is_none() {
399 eprintln!(
400 "[profile_extractor] #{element_id} ({ifc_type}): missing Depth, defaulting to 1.0"
401 );
402 }
403 let depth = raw_depth.unwrap_or(1.0) * unit_scale;
404
405 let combined_ifc = elem_transform * solid_transform;
407
408 let transform = convert_ifc_to_webgl(&combined_ifc);
410
411 let world_dir_ifc = combined_ifc.transform_vector(&local_dir);
413
414 let extrusion_dir = [
416 world_dir_ifc.x as f32,
417 world_dir_ifc.z as f32, -world_dir_ifc.y as f32, ];
420
421 let outer_points: Vec<f32> = profile
423 .outer
424 .iter()
425 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
426 .collect();
427
428 let hole_counts: Vec<u32> = profile.holes.iter().map(|h| h.len() as u32).collect();
429 let hole_points: Vec<f32> = profile
430 .holes
431 .iter()
432 .flat_map(|h| {
433 h.iter()
434 .flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
435 })
436 .collect();
437
438 Ok(ExtractedProfile {
439 express_id: element_id,
440 ifc_type: ifc_type.to_string(),
441 outer_points,
442 hole_counts,
443 hole_points,
444 transform,
445 extrusion_dir,
446 extrusion_depth: depth as f32,
447 model_index,
448 })
449}
450
451fn get_placement_transform(
458 placement_attr: Option<&AttributeValue>,
459 decoder: &mut EntityDecoder,
460) -> Matrix4<f64> {
461 let attr = match placement_attr {
462 Some(a) if !a.is_null() => a,
463 _ => return Matrix4::identity(),
464 };
465 match decoder.resolve_ref(attr) {
466 Ok(Some(p)) => get_placement_recursive(&p, decoder, 0),
467 _ => Matrix4::identity(),
468 }
469}
470
471const MAX_PLACEMENT_DEPTH: usize = 100;
472
473fn get_placement_recursive(
474 placement: &DecodedEntity,
475 decoder: &mut EntityDecoder,
476 depth: usize,
477) -> Matrix4<f64> {
478 if depth > MAX_PLACEMENT_DEPTH || placement.ifc_type != IfcType::IfcLocalPlacement {
479 return Matrix4::identity();
480 }
481
482 let parent_tf = if let Some(parent_attr) = placement.get(0) {
484 if !parent_attr.is_null() {
485 match decoder.resolve_ref(parent_attr) {
486 Ok(Some(parent)) => get_placement_recursive(&parent, decoder, depth + 1),
487 _ => Matrix4::identity(),
488 }
489 } else {
490 Matrix4::identity()
491 }
492 } else {
493 Matrix4::identity()
494 };
495
496 let local_tf = if let Some(rel_attr) = placement.get(1) {
498 if !rel_attr.is_null() {
499 match decoder.resolve_ref(rel_attr) {
500 Ok(Some(rel)) if rel.ifc_type == IfcType::IfcAxis2Placement3D => {
501 parse_axis2_placement_3d(&rel, decoder).unwrap_or(Matrix4::identity())
502 }
503 _ => Matrix4::identity(),
504 }
505 } else {
506 Matrix4::identity()
507 }
508 } else {
509 Matrix4::identity()
510 };
511
512 parent_tf * local_tf
513}
514
515fn parse_axis2_placement_3d(
522 placement: &DecodedEntity,
523 decoder: &mut EntityDecoder,
524) -> Result<Matrix4<f64>> {
525 let location =
527 parse_cartesian_point(placement, decoder, 0).unwrap_or(Point3::new(0.0, 0.0, 0.0));
528
529 let z_axis = if let Some(a) = placement.get(1) {
531 if !a.is_null() {
532 decoder
533 .resolve_ref(a)?
534 .map(|e| parse_direction_entity(&e))
535 .transpose()?
536 .unwrap_or(Vector3::new(0.0, 0.0, 1.0))
537 } else {
538 Vector3::new(0.0, 0.0, 1.0)
539 }
540 } else {
541 Vector3::new(0.0, 0.0, 1.0)
542 };
543
544 let x_axis_raw = if let Some(a) = placement.get(2) {
546 if !a.is_null() {
547 decoder
548 .resolve_ref(a)?
549 .map(|e| parse_direction_entity(&e))
550 .transpose()?
551 .unwrap_or(Vector3::new(1.0, 0.0, 0.0))
552 } else {
553 Vector3::new(1.0, 0.0, 0.0)
554 }
555 } else {
556 Vector3::new(1.0, 0.0, 0.0)
557 };
558
559 let z = z_axis.normalize();
560
561 let dot = x_axis_raw.dot(&z);
563 let x_orth = x_axis_raw - z * dot;
564 let x = if x_orth.norm() > 1e-6 {
565 x_orth.normalize()
566 } else {
567 if z.z.abs() < 0.9 {
569 Vector3::new(0.0, 0.0, 1.0).cross(&z).normalize()
570 } else {
571 Vector3::new(1.0, 0.0, 0.0).cross(&z).normalize()
572 }
573 };
574 let y = z.cross(&x).normalize();
575
576 #[rustfmt::skip]
578 let m = Matrix4::new(
579 x.x, y.x, z.x, location.x,
580 x.y, y.y, z.y, location.y,
581 x.z, y.z, z.z, location.z,
582 0.0, 0.0, 0.0, 1.0,
583 );
584 Ok(m)
585}
586
587fn parse_cartesian_point(
589 parent: &DecodedEntity,
590 decoder: &mut EntityDecoder,
591 attr_index: usize,
592) -> Result<Point3<f64>> {
593 let pt_attr = parent
594 .get(attr_index)
595 .ok_or_else(|| Error::geometry("Missing cartesian point attr"))?;
596
597 if pt_attr.is_null() {
598 return Ok(Point3::new(0.0, 0.0, 0.0));
599 }
600
601 let pt_entity = decoder
602 .resolve_ref(pt_attr)?
603 .ok_or_else(|| Error::geometry("Failed to resolve IfcCartesianPoint"))?;
604
605 let coords = pt_entity
606 .get(0)
607 .and_then(|a| a.as_list())
608 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates"))?;
609
610 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
611 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
612 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
613
614 Ok(Point3::new(x, y, z))
615}
616
617fn parse_direction_entity(entity: &DecodedEntity) -> Result<Vector3<f64>> {
619 let ratios = entity
620 .get(0)
621 .and_then(|a| a.as_list())
622 .ok_or_else(|| Error::geometry("IfcDirection missing ratios"))?;
623
624 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
625 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
626 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
627
628 Ok(Vector3::new(x, y, z).normalize())
629}
630
631fn parse_extrusion_direction(solid: &DecodedEntity, decoder: &mut EntityDecoder) -> Vector3<f64> {
633 let default = Vector3::new(0.0, 0.0, 1.0);
634 let dir_attr = match solid.get(2) {
635 Some(a) if !a.is_null() => a,
636 _ => return default,
637 };
638 let dir_ent = match decoder.resolve_ref(dir_attr) {
639 Ok(Some(e)) => e,
640 _ => return default,
641 };
642 let ratios = match dir_ent.get(0).and_then(|a| a.as_list()) {
643 Some(r) => r,
644 None => return default,
645 };
646 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
647 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
648 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
649 let v = Vector3::new(x, y, z);
650 let len = v.norm();
651 if len > 1e-10 {
652 v / len
653 } else {
654 default
655 }
656}
657
658fn scale_translation(mut m: Matrix4<f64>, scale: f64) -> Matrix4<f64> {
664 if scale != 1.0 {
665 m[(0, 3)] *= scale;
666 m[(1, 3)] *= scale;
667 m[(2, 3)] *= scale;
668 }
669 m
670}
671
672fn convert_ifc_to_webgl(m: &Matrix4<f64>) -> [f32; 16] {
677 let mut result = [0.0f32; 16];
678 for col in 0..4 {
679 result[col * 4 + 0] = 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; }
684 result
685}
686
687fn detect_unit_scale(content: &str, decoder: &mut EntityDecoder) -> f64 {
689 let mut scanner = EntityScanner::new(content);
690 while let Some((id, type_name, _, _)) = scanner.next_entity() {
691 if type_name == "IFCPROJECT" {
692 if let Ok(scale) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
693 return scale;
694 }
695 break;
696 }
697 }
698 1.0
699}