use crate::profiles::ProfileProcessor;
use crate::{Error, Point3, Result, Vector3};
use ifc_lite_core::{
build_entity_index, AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcSchema,
IfcType,
};
use nalgebra::Matrix4;
#[derive(Debug, Clone)]
pub struct ExtractedProfile {
pub express_id: u32,
pub ifc_type: String,
pub outer_points: Vec<f32>,
pub hole_counts: Vec<u32>,
pub hole_points: Vec<f32>,
pub transform: [f32; 16],
pub extrusion_dir: [f32; 3],
pub extrusion_depth: f32,
pub model_index: u32,
}
pub fn extract_profiles(content: &str, model_index: u32) -> Vec<ExtractedProfile> {
let entity_index = build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, entity_index);
let unit_scale = detect_unit_scale(content, &mut decoder);
let schema = IfcSchema::new();
let profile_processor = ProfileProcessor::new(schema);
let mut results = Vec::new();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if !ifc_lite_core::has_geometry_by_name(type_name) {
continue;
}
let entity = match decoder.decode_at_with_id(id, start, end) {
Ok(e) => e,
Err(_) => continue,
};
let element_transform = get_placement_transform(entity.get(5), &mut decoder);
let elem_tf = scale_translation(element_transform, unit_scale);
let repr_attr = match entity.get(6) {
Some(a) if !a.is_null() => a,
_ => continue,
};
let repr = match decoder.resolve_ref(repr_attr) {
Ok(Some(r)) => r,
_ => continue,
};
let reprs_attr = match repr.get(2) {
Some(a) => a,
None => continue,
};
let representations = match decoder.resolve_ref_list(reprs_attr) {
Ok(r) => r,
Err(_) => continue,
};
let ifc_type_name = entity.ifc_type.name().to_string();
for shape_rep in representations {
if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
continue;
}
let rep_id = shape_rep.get(1).and_then(|a| a.as_string()).unwrap_or("");
if rep_id != "Body" && rep_id != "SweptSolid" {
continue;
}
let items_attr = match shape_rep.get(3) {
Some(a) => a,
None => continue,
};
let items = match decoder.resolve_ref_list(items_attr) {
Ok(i) => i,
Err(_) => continue,
};
for item in &items {
if item.ifc_type == IfcType::IfcExtrudedAreaSolid {
match extract_extruded_solid(
id,
&ifc_type_name,
item,
&elem_tf,
unit_scale,
&profile_processor,
&mut decoder,
model_index,
) {
Ok(entry) => results.push(entry),
Err(_e) => {
#[cfg(feature = "debug_geometry")]
eprintln!("[profile_extractor] Skipping #{id} ({ifc_type_name}): {_e}");
}
}
} else if item.ifc_type == IfcType::IfcMappedItem {
extract_mapped_item_profiles(
id,
&ifc_type_name,
item,
&elem_tf,
unit_scale,
&profile_processor,
&mut decoder,
model_index,
0,
&mut results,
);
}
}
}
}
results
}
const MAX_MAPPED_DEPTH: usize = 3;
fn extract_mapped_item_profiles(
element_id: u32,
ifc_type: &str,
mapped_item: &DecodedEntity,
elem_transform: &Matrix4<f64>,
unit_scale: f64,
profile_processor: &ProfileProcessor,
decoder: &mut EntityDecoder,
model_index: u32,
depth: usize,
results: &mut Vec<ExtractedProfile>,
) {
if depth > MAX_MAPPED_DEPTH {
#[cfg(feature = "debug_geometry")]
eprintln!("[profile_extractor] #{element_id} ({ifc_type}): max mapped item depth exceeded");
return;
}
let source = match mapped_item
.get(0)
.and_then(|a| if a.is_null() { None } else { Some(a) })
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
{
Some(s) => s,
None => return,
};
let target_tf = mapped_item
.get(1)
.and_then(|a| if a.is_null() { None } else { Some(a) })
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|e| parse_cartesian_transformation_operator(&e, decoder).ok())
.unwrap_or_else(Matrix4::identity);
let scaled_target = scale_translation(target_tf, unit_scale);
let composed = elem_transform * scaled_target;
let mapped_rep = match source
.get(1)
.and_then(|a| if a.is_null() { None } else { Some(a) })
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
{
Some(r) => r,
None => return,
};
let items = match mapped_rep
.get(3)
.and_then(|a| decoder.resolve_ref_list(a).ok())
{
Some(i) => i,
None => return,
};
for sub_item in &items {
if sub_item.ifc_type == IfcType::IfcExtrudedAreaSolid {
match extract_extruded_solid(
element_id,
ifc_type,
sub_item,
&composed,
unit_scale,
profile_processor,
decoder,
model_index,
) {
Ok(entry) => results.push(entry),
Err(_e) => {
#[cfg(feature = "debug_geometry")]
eprintln!("[profile_extractor] #{element_id} ({ifc_type}) mapped: {_e}");
}
}
} else if sub_item.ifc_type == IfcType::IfcMappedItem {
extract_mapped_item_profiles(
element_id,
ifc_type,
sub_item,
&composed,
unit_scale,
profile_processor,
decoder,
model_index,
depth + 1,
results,
);
}
}
}
fn parse_cartesian_transformation_operator(
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Matrix4<f64>> {
let origin = parse_cartesian_point(entity, decoder, 2).unwrap_or(Point3::new(0.0, 0.0, 0.0));
let scale = entity.get(3).and_then(|v| v.as_float()).unwrap_or(1.0);
let x_axis = entity
.get(0)
.filter(|a| !a.is_null())
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|e| parse_direction_entity(&e).ok())
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0))
.normalize();
let z_axis = entity
.get(4)
.filter(|a| !a.is_null())
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|e| parse_direction_entity(&e).ok())
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0))
.normalize();
let y_axis = z_axis.cross(&x_axis).normalize();
let x_axis = y_axis.cross(&z_axis).normalize();
#[rustfmt::skip]
let m = Matrix4::new(
x_axis.x * scale, y_axis.x * scale, z_axis.x * scale, origin.x,
x_axis.y * scale, y_axis.y * scale, z_axis.y * scale, origin.y,
x_axis.z * scale, y_axis.z * scale, z_axis.z * scale, origin.z,
0.0, 0.0, 0.0, 1.0,
);
Ok(m)
}
fn extract_extruded_solid(
element_id: u32,
ifc_type: &str,
solid: &DecodedEntity,
elem_transform: &Matrix4<f64>,
unit_scale: f64,
profile_processor: &ProfileProcessor,
decoder: &mut EntityDecoder,
model_index: u32,
) -> Result<ExtractedProfile> {
let profile_attr = solid
.get(0)
.ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea"))?;
let profile_entity = decoder
.resolve_ref(profile_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve SweptArea"))?;
let profile = profile_processor.process(&profile_entity, decoder)?;
if profile.outer.is_empty() {
return Err(Error::geometry("empty profile"));
}
let solid_transform = if let Some(pos_attr) = solid.get(1) {
if !pos_attr.is_null() {
if let Some(pos_ent) = decoder.resolve_ref(pos_attr)? {
if pos_ent.ifc_type == IfcType::IfcAxis2Placement3D {
let mut t = parse_axis2_placement_3d(&pos_ent, decoder)?;
t[(0, 3)] *= unit_scale;
t[(1, 3)] *= unit_scale;
t[(2, 3)] *= unit_scale;
t
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
let local_dir = parse_extrusion_direction(solid, decoder);
let raw_depth = solid.get(3).and_then(|v| v.as_float());
#[cfg(feature = "debug_geometry")]
if raw_depth.is_none() {
eprintln!(
"[profile_extractor] #{element_id} ({ifc_type}): missing Depth, defaulting to 1.0"
);
}
let depth = raw_depth.unwrap_or(1.0) * unit_scale;
let combined_ifc = elem_transform * solid_transform;
let transform = convert_ifc_to_webgl(&combined_ifc);
let world_dir_ifc = combined_ifc.transform_vector(&local_dir);
let extrusion_dir = [
world_dir_ifc.x as f32,
world_dir_ifc.z as f32, -world_dir_ifc.y as f32, ];
let outer_points: Vec<f32> = profile
.outer
.iter()
.flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
.collect();
let hole_counts: Vec<u32> = profile.holes.iter().map(|h| h.len() as u32).collect();
let hole_points: Vec<f32> = profile
.holes
.iter()
.flat_map(|h| {
h.iter()
.flat_map(|p| [(p.x * unit_scale) as f32, (p.y * unit_scale) as f32])
})
.collect();
Ok(ExtractedProfile {
express_id: element_id,
ifc_type: ifc_type.to_string(),
outer_points,
hole_counts,
hole_points,
transform,
extrusion_dir,
extrusion_depth: depth as f32,
model_index,
})
}
fn get_placement_transform(
placement_attr: Option<&AttributeValue>,
decoder: &mut EntityDecoder,
) -> Matrix4<f64> {
let attr = match placement_attr {
Some(a) if !a.is_null() => a,
_ => return Matrix4::identity(),
};
match decoder.resolve_ref(attr) {
Ok(Some(p)) => get_placement_recursive(&p, decoder, 0),
_ => Matrix4::identity(),
}
}
const MAX_PLACEMENT_DEPTH: usize = 100;
fn get_placement_recursive(
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: usize,
) -> Matrix4<f64> {
if depth > MAX_PLACEMENT_DEPTH || placement.ifc_type != IfcType::IfcLocalPlacement {
return Matrix4::identity();
}
let parent_tf = if let Some(parent_attr) = placement.get(0) {
if !parent_attr.is_null() {
match decoder.resolve_ref(parent_attr) {
Ok(Some(parent)) => get_placement_recursive(&parent, decoder, depth + 1),
_ => Matrix4::identity(),
}
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
let local_tf = if let Some(rel_attr) = placement.get(1) {
if !rel_attr.is_null() {
match decoder.resolve_ref(rel_attr) {
Ok(Some(rel)) if rel.ifc_type == IfcType::IfcAxis2Placement3D => {
parse_axis2_placement_3d(&rel, decoder).unwrap_or(Matrix4::identity())
}
_ => Matrix4::identity(),
}
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
parent_tf * local_tf
}
fn parse_axis2_placement_3d(
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Matrix4<f64>> {
let location =
parse_cartesian_point(placement, decoder, 0).unwrap_or(Point3::new(0.0, 0.0, 0.0));
let z_axis = if let Some(a) = placement.get(1) {
if !a.is_null() {
decoder
.resolve_ref(a)?
.map(|e| parse_direction_entity(&e))
.transpose()?
.unwrap_or(Vector3::new(0.0, 0.0, 1.0))
} else {
Vector3::new(0.0, 0.0, 1.0)
}
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let x_axis_raw = if let Some(a) = placement.get(2) {
if !a.is_null() {
decoder
.resolve_ref(a)?
.map(|e| parse_direction_entity(&e))
.transpose()?
.unwrap_or(Vector3::new(1.0, 0.0, 0.0))
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let z = z_axis.normalize();
let dot = x_axis_raw.dot(&z);
let x_orth = x_axis_raw - z * dot;
let x = if x_orth.norm() > 1e-6 {
x_orth.normalize()
} else {
if z.z.abs() < 0.9 {
Vector3::new(0.0, 0.0, 1.0).cross(&z).normalize()
} else {
Vector3::new(1.0, 0.0, 0.0).cross(&z).normalize()
}
};
let y = z.cross(&x).normalize();
#[rustfmt::skip]
let m = Matrix4::new(
x.x, y.x, z.x, location.x,
x.y, y.y, z.y, location.y,
x.z, y.z, z.z, location.z,
0.0, 0.0, 0.0, 1.0,
);
Ok(m)
}
fn parse_cartesian_point(
parent: &DecodedEntity,
decoder: &mut EntityDecoder,
attr_index: usize,
) -> Result<Point3<f64>> {
let pt_attr = parent
.get(attr_index)
.ok_or_else(|| Error::geometry("Missing cartesian point attr"))?;
if pt_attr.is_null() {
return Ok(Point3::new(0.0, 0.0, 0.0));
}
let pt_entity = decoder
.resolve_ref(pt_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve IfcCartesianPoint"))?;
let coords = pt_entity
.get(0)
.and_then(|a| a.as_list())
.ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates"))?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Ok(Point3::new(x, y, z))
}
fn parse_direction_entity(entity: &DecodedEntity) -> Result<Vector3<f64>> {
let ratios = entity
.get(0)
.and_then(|a| a.as_list())
.ok_or_else(|| Error::geometry("IfcDirection missing ratios"))?;
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
Ok(Vector3::new(x, y, z).normalize())
}
fn parse_extrusion_direction(solid: &DecodedEntity, decoder: &mut EntityDecoder) -> Vector3<f64> {
let default = Vector3::new(0.0, 0.0, 1.0);
let dir_attr = match solid.get(2) {
Some(a) if !a.is_null() => a,
_ => return default,
};
let dir_ent = match decoder.resolve_ref(dir_attr) {
Ok(Some(e)) => e,
_ => return default,
};
let ratios = match dir_ent.get(0).and_then(|a| a.as_list()) {
Some(r) => r,
None => return default,
};
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
let v = Vector3::new(x, y, z);
let len = v.norm();
if len > 1e-10 {
v / len
} else {
default
}
}
fn scale_translation(mut m: Matrix4<f64>, scale: f64) -> Matrix4<f64> {
if scale != 1.0 {
m[(0, 3)] *= scale;
m[(1, 3)] *= scale;
m[(2, 3)] *= scale;
}
m
}
fn convert_ifc_to_webgl(m: &Matrix4<f64>) -> [f32; 16] {
let mut result = [0.0f32; 16];
for col in 0..4 {
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; }
result
}
fn detect_unit_scale(content: &str, decoder: &mut EntityDecoder) -> f64 {
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, _, _)) = scanner.next_entity() {
if type_name == "IFCPROJECT" {
if let Ok(scale) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
return scale;
}
break;
}
}
1.0
}