use crate::profile::Profile2D;
use crate::{Error, Point2, Point3, Result, Vector3};
use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcSchema, IfcType, ProfileCategory};
use std::f64::consts::PI;
const MAX_CURVE_DEPTH: u32 = 50;
pub struct ProfileProcessor {
schema: IfcSchema,
}
impl ProfileProcessor {
pub fn new(schema: IfcSchema) -> Self {
Self { schema }
}
#[inline]
pub fn process(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
match profile.ifc_type {
IfcType::IfcDerivedProfileDef | IfcType::IfcMirroredProfileDef => {
self.process_derived(profile, decoder)
}
_ => match self.schema.profile_category(&profile.ifc_type) {
Some(ProfileCategory::Parametric) => self.process_parametric(profile, decoder),
Some(ProfileCategory::Arbitrary) => self.process_arbitrary(profile, decoder),
Some(ProfileCategory::Composite) => self.process_composite(profile, decoder),
_ => Err(Error::geometry(format!(
"Unsupported profile type: {}",
profile.ifc_type
))),
},
}
}
#[inline]
fn process_parametric(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let mut base_profile = match profile.ifc_type {
IfcType::IfcRectangleProfileDef => self.process_rectangle(profile),
IfcType::IfcCircleProfileDef => self.process_circle(profile),
IfcType::IfcCircleHollowProfileDef => self.process_circle_hollow(profile),
IfcType::IfcRectangleHollowProfileDef => self.process_rectangle_hollow(profile),
IfcType::IfcIShapeProfileDef => self.process_i_shape(profile),
IfcType::IfcLShapeProfileDef => self.process_l_shape(profile),
IfcType::IfcUShapeProfileDef => self.process_u_shape(profile),
IfcType::IfcTShapeProfileDef => self.process_t_shape(profile),
IfcType::IfcCShapeProfileDef => self.process_c_shape(profile),
IfcType::IfcZShapeProfileDef => self.process_z_shape(profile),
_ => Err(Error::geometry(format!(
"Unsupported parametric profile: {}",
profile.ifc_type
))),
}?;
if let Some(pos_attr) = profile.get(2) {
if !pos_attr.is_null() {
if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
if pos_entity.ifc_type == IfcType::IfcAxis2Placement2D {
self.apply_profile_position(&mut base_profile, &pos_entity, decoder)?;
}
}
}
}
Ok(base_profile)
}
fn apply_profile_position(
&self,
profile: &mut Profile2D,
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<()> {
let (loc_x, loc_y) = if let Some(loc_attr) = placement.get(0) {
if !loc_attr.is_null() {
if let Some(loc_entity) = decoder.resolve_ref(loc_attr)? {
let coords = loc_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing point coordinates".to_string()))?;
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);
(x, y)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
let (dir_x, dir_y) = if let Some(dir_attr) = placement.get(1) {
if !dir_attr.is_null() {
if let Some(dir_entity) = decoder.resolve_ref(dir_attr)? {
let ratios = dir_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing direction ratios".to_string()))?;
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let len = (x * x + y * y).sqrt();
if len > 1e-10 {
(x / len, y / len)
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
};
if loc_x.abs() < 1e-10
&& loc_y.abs() < 1e-10
&& (dir_x - 1.0).abs() < 1e-10
&& dir_y.abs() < 1e-10
{
return Ok(());
}
let x_axis = (dir_x, dir_y);
let y_axis = (-dir_y, dir_x);
for point in &mut profile.outer {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
}
for hole in &mut profile.holes {
for point in hole {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
}
}
Ok(())
}
fn process_derived(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let parent_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Derived profile missing ParentProfile".to_string()))?;
let parent_profile = decoder.resolve_ref(parent_attr)?.ok_or_else(|| {
Error::geometry("Derived profile ParentProfile not found".to_string())
})?;
let mut result = self.process(&parent_profile, decoder)?;
let operator_attr = profile
.get(3)
.ok_or_else(|| Error::geometry("Derived profile missing Operator".to_string()))?;
let operator = decoder
.resolve_ref(operator_attr)?
.ok_or_else(|| Error::geometry("Derived profile Operator not found".to_string()))?;
self.apply_cartesian_transformation_operator_2d(&mut result, &operator, decoder)?;
Ok(result)
}
fn apply_cartesian_transformation_operator_2d(
&self,
profile: &mut Profile2D,
operator: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<()> {
let (origin_x, origin_y) = if let Some(origin_attr) = operator.get(2) {
if let Some(origin_entity) = decoder.resolve_ref(origin_attr)? {
let coords = origin_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| {
Error::geometry("Missing operator origin coordinates".to_string())
})?;
(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
let scale_x = operator.get_float(3).unwrap_or(1.0);
let scale_y = match operator.ifc_type {
IfcType::IfcCartesianTransformationOperator2DnonUniform => {
operator.get_float(4).unwrap_or(scale_x)
}
_ => scale_x,
};
let axis1 = self.parse_operator_axis_2d(operator.get(0), decoder, (1.0, 0.0))?;
let axis2 = self.parse_operator_axis_2d(operator.get(1), decoder, (0.0, 1.0))?;
let (x_axis, y_axis) = match (axis1, axis2) {
(Some(x_axis), Some(y_axis)) => (x_axis, y_axis),
(Some(x_axis), None) => (x_axis, (-x_axis.1, x_axis.0)),
(None, Some(y_axis)) => ((y_axis.1, -y_axis.0), y_axis),
(None, None) => ((1.0, 0.0), (0.0, 1.0)),
};
for point in &mut profile.outer {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 * scale_x + old_y * y_axis.0 * scale_y + origin_x;
point.y = old_x * x_axis.1 * scale_x + old_y * y_axis.1 * scale_y + origin_y;
}
for hole in &mut profile.holes {
for point in hole {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 * scale_x + old_y * y_axis.0 * scale_y + origin_x;
point.y = old_x * x_axis.1 * scale_x + old_y * y_axis.1 * scale_y + origin_y;
}
}
let det = scale_x * scale_y * (x_axis.0 * y_axis.1 - y_axis.0 * x_axis.1);
if det < 0.0 {
profile.outer.reverse();
for hole in &mut profile.holes {
hole.reverse();
}
}
Ok(())
}
fn parse_operator_axis_2d(
&self,
axis_attr: Option<&AttributeValue>,
decoder: &mut EntityDecoder,
default: (f64, f64),
) -> Result<Option<(f64, f64)>> {
let Some(axis_attr) = axis_attr else {
return Ok(None);
};
if axis_attr.is_null() {
return Ok(None);
}
let Some(axis_entity) = decoder.resolve_ref(axis_attr)? else {
return Ok(None);
};
let ratios = axis_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing operator axis ratios".to_string()))?;
let x = ratios
.first()
.and_then(|v| v.as_float())
.unwrap_or(default.0);
let y = ratios
.get(1)
.and_then(|v| v.as_float())
.unwrap_or(default.1);
let len = (x * x + y * y).sqrt();
if len <= 1e-10 {
return Ok(Some(default));
}
Ok(Some((x / len, y / len)))
}
#[inline]
fn process_rectangle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let x_dim = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Rectangle missing XDim".to_string()))?;
let y_dim = profile
.get_float(4)
.ok_or_else(|| Error::geometry("Rectangle missing YDim".to_string()))?;
let half_x = x_dim / 2.0;
let half_y = y_dim / 2.0;
let points = vec![
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
Ok(Profile2D::new(points))
}
#[inline]
fn process_circle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let radius = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
let segments = 36;
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
points.push(Point2::new(x, y));
}
Ok(Profile2D::new(points))
}
fn process_i_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let overall_width = profile
.get_float(3)
.ok_or_else(|| Error::geometry("I-Shape missing OverallWidth".to_string()))?;
let overall_depth = profile
.get_float(4)
.ok_or_else(|| Error::geometry("I-Shape missing OverallDepth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("I-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("I-Shape missing FlangeThickness".to_string()))?;
let half_width = overall_width / 2.0;
let half_depth = overall_depth / 2.0;
let half_web = web_thickness / 2.0;
let points = vec![
Point2::new(-half_width, -half_depth),
Point2::new(half_width, -half_depth),
Point2::new(half_width, -half_depth + flange_thickness),
Point2::new(half_web, -half_depth + flange_thickness),
Point2::new(half_web, half_depth - flange_thickness),
Point2::new(half_width, half_depth - flange_thickness),
Point2::new(half_width, half_depth),
Point2::new(-half_width, half_depth),
Point2::new(-half_width, half_depth - flange_thickness),
Point2::new(-half_web, half_depth - flange_thickness),
Point2::new(-half_web, -half_depth + flange_thickness),
Point2::new(-half_width, -half_depth + flange_thickness),
];
Ok(Profile2D::new(points))
}
fn process_circle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let radius = profile
.get_float(3)
.ok_or_else(|| Error::geometry("CircleHollow missing Radius".to_string()))?;
let wall_thickness = profile
.get_float(4)
.ok_or_else(|| Error::geometry("CircleHollow missing WallThickness".to_string()))?;
let inner_radius = radius - wall_thickness;
let segments = 36;
let mut outer_points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
outer_points.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
}
let mut inner_points = Vec::with_capacity(segments);
for i in (0..segments).rev() {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
inner_points.push(Point2::new(
inner_radius * angle.cos(),
inner_radius * angle.sin(),
));
}
let mut result = Profile2D::new(outer_points);
result.add_hole(inner_points);
Ok(result)
}
fn process_rectangle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let x_dim = profile
.get_float(3)
.ok_or_else(|| Error::geometry("RectangleHollow missing XDim".to_string()))?;
let y_dim = profile
.get_float(4)
.ok_or_else(|| Error::geometry("RectangleHollow missing YDim".to_string()))?;
let wall_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("RectangleHollow missing WallThickness".to_string()))?;
let half_x = x_dim / 2.0;
let half_y = y_dim / 2.0;
if wall_thickness >= half_x || wall_thickness >= half_y {
return Err(Error::geometry(format!(
"RectangleHollow WallThickness {} exceeds half dimensions ({}, {})",
wall_thickness, half_x, half_y
)));
}
let inner_half_x = half_x - wall_thickness;
let inner_half_y = half_y - wall_thickness;
let outer_points = vec![
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
let inner_points = vec![
Point2::new(-inner_half_x, -inner_half_y),
Point2::new(-inner_half_x, inner_half_y),
Point2::new(inner_half_x, inner_half_y),
Point2::new(inner_half_x, -inner_half_y),
];
let mut result = Profile2D::new(outer_points);
result.add_hole(inner_points);
Ok(result)
}
fn process_l_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("L-Shape missing Depth".to_string()))?;
let width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("L-Shape missing Width".to_string()))?;
let thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("L-Shape missing Thickness".to_string()))?;
let points = vec![
Point2::new(0.0, 0.0),
Point2::new(width, 0.0),
Point2::new(width, thickness),
Point2::new(thickness, thickness),
Point2::new(thickness, depth),
Point2::new(0.0, depth),
];
Ok(Profile2D::new(points))
}
fn process_u_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("U-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("U-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("U-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("U-Shape missing FlangeThickness".to_string()))?;
let half_depth = depth / 2.0;
let points = vec![
Point2::new(0.0, -half_depth),
Point2::new(flange_width, -half_depth),
Point2::new(flange_width, -half_depth + flange_thickness),
Point2::new(web_thickness, -half_depth + flange_thickness),
Point2::new(web_thickness, half_depth - flange_thickness),
Point2::new(flange_width, half_depth - flange_thickness),
Point2::new(flange_width, half_depth),
Point2::new(0.0, half_depth),
];
Ok(Profile2D::new(points))
}
fn process_t_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("T-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("T-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("T-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("T-Shape missing FlangeThickness".to_string()))?;
let half_flange = flange_width / 2.0;
let half_web = web_thickness / 2.0;
let points = vec![
Point2::new(-half_web, 0.0),
Point2::new(-half_web, depth - flange_thickness),
Point2::new(-half_flange, depth - flange_thickness),
Point2::new(-half_flange, depth),
Point2::new(half_flange, depth),
Point2::new(half_flange, depth - flange_thickness),
Point2::new(half_web, depth - flange_thickness),
Point2::new(half_web, 0.0),
];
Ok(Profile2D::new(points))
}
fn process_c_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("C-Shape missing Depth".to_string()))?;
let _width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("C-Shape missing Width".to_string()))?;
let wall_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("C-Shape missing WallThickness".to_string()))?;
let girth = profile.get_float(6).unwrap_or(wall_thickness * 2.0);
let half_depth = depth / 2.0;
let points = vec![
Point2::new(girth, -half_depth),
Point2::new(0.0, -half_depth),
Point2::new(0.0, half_depth),
Point2::new(girth, half_depth),
Point2::new(girth, half_depth - wall_thickness),
Point2::new(wall_thickness, half_depth - wall_thickness),
Point2::new(wall_thickness, -half_depth + wall_thickness),
Point2::new(girth, -half_depth + wall_thickness),
];
Ok(Profile2D::new(points))
}
fn process_z_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Z-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("Z-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("Z-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("Z-Shape missing FlangeThickness".to_string()))?;
let half_depth = depth / 2.0;
let half_web = web_thickness / 2.0;
let points = vec![
Point2::new(-half_web, -half_depth),
Point2::new(-half_web - flange_width, -half_depth),
Point2::new(-half_web - flange_width, -half_depth + flange_thickness),
Point2::new(-half_web, -half_depth + flange_thickness),
Point2::new(-half_web, half_depth - flange_thickness),
Point2::new(half_web, half_depth - flange_thickness),
Point2::new(half_web, half_depth),
Point2::new(half_web + flange_width, half_depth),
Point2::new(half_web + flange_width, half_depth - flange_thickness),
Point2::new(half_web, half_depth - flange_thickness),
Point2::new(half_web, -half_depth + flange_thickness),
Point2::new(-half_web, -half_depth + flange_thickness),
];
Ok(Profile2D::new(points))
}
fn process_arbitrary(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let curve_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Arbitrary profile missing OuterCurve".to_string()))?;
let curve = decoder
.resolve_ref(curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve OuterCurve".to_string()))?;
let outer_points = self.process_curve(&curve, decoder)?;
let mut result = Profile2D::new(outer_points);
if profile.ifc_type == IfcType::IfcArbitraryProfileDefWithVoids {
if let Some(inner_curves_attr) = profile.get(3) {
let inner_curves = decoder.resolve_ref_list(inner_curves_attr)?;
for inner_curve in inner_curves {
let hole_points = self.process_curve(&inner_curve, decoder)?;
result.add_hole(hole_points);
}
}
}
Ok(result)
}
#[inline]
fn process_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
self.process_curve_with_depth(curve, decoder, 0)
}
fn process_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
if depth > MAX_CURVE_DEPTH {
return Err(Error::geometry(format!(
"Curve nesting depth {} exceeds limit {}",
depth, MAX_CURVE_DEPTH
)));
}
match curve.ifc_type {
IfcType::IfcPolyline => self.process_polyline(curve, decoder),
IfcType::IfcIndexedPolyCurve => self.process_indexed_polycurve(curve, decoder),
IfcType::IfcCompositeCurve => self.process_composite_curve_with_depth(curve, decoder, depth),
IfcType::IfcTrimmedCurve => self.process_trimmed_curve_with_depth(curve, decoder, depth),
IfcType::IfcCircle => self.process_circle_curve(curve, decoder),
IfcType::IfcEllipse => self.process_ellipse_curve(curve, decoder),
_ => Err(Error::geometry(format!(
"Unsupported curve type: {}",
curve.ifc_type
))),
}
}
#[inline]
pub fn get_curve_points(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
self.get_curve_points_with_depth(curve, decoder, 0)
}
fn get_curve_points_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point3<f64>>> {
if depth > MAX_CURVE_DEPTH {
return Err(Error::geometry(format!(
"Curve nesting depth {} exceeds limit {}",
depth, MAX_CURVE_DEPTH
)));
}
match curve.ifc_type {
IfcType::IfcPolyline => self.process_polyline_3d(curve, decoder),
IfcType::IfcCompositeCurve => self.process_composite_curve_3d_with_depth(curve, decoder, depth),
IfcType::IfcCircle => self.process_circle_3d(curve, decoder),
IfcType::IfcTrimmedCurve => {
let points_2d = self.process_trimmed_curve_with_depth(curve, decoder, depth)?;
Ok(points_2d
.into_iter()
.map(|p| Point3::new(p.x, p.y, 0.0))
.collect())
}
_ => {
let points_2d = self.process_curve_with_depth(curve, decoder, depth)?;
Ok(points_2d
.into_iter()
.map(|p| Point3::new(p.x, p.y, 0.0))
.collect())
}
}
}
fn process_circle_3d(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
let position_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("Circle missing Position".to_string()))?;
let radius = curve
.get_float(1)
.ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
let position = decoder
.resolve_ref(position_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve circle position".to_string()))?;
let (center, x_axis, y_axis) = if position.ifc_type == IfcType::IfcAxis2Placement3D {
let loc_attr = position
.get(0)
.ok_or_else(|| Error::geometry("Axis2Placement3D missing Location".to_string()))?;
let loc = decoder
.resolve_ref(loc_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve location".to_string()))?;
let coords = loc
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Location missing coordinates".to_string()))?;
let center = Point3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
);
let z_axis = if let Some(axis_attr) = position.get(1) {
if !axis_attr.is_null() {
let axis = decoder.resolve_ref(axis_attr)?;
if let Some(axis) = axis {
let coords = axis.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
Vector3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(1.0),
)
.normalize()
} else {
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)
}
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let x_axis = if let Some(ref_attr) = position.get(2) {
if !ref_attr.is_null() {
let ref_dir = decoder.resolve_ref(ref_attr)?;
if let Some(ref_dir) = ref_dir {
let coords = ref_dir.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
Vector3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(1.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
)
.normalize()
} else {
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)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let y_axis = z_axis.cross(&x_axis).normalize();
(center, x_axis, y_axis)
} else {
let loc_attr = position.get(0);
let (cx, cy) = if let Some(attr) = loc_attr {
let loc = decoder.resolve_ref(attr)?;
if let Some(loc) = loc {
let coords = loc.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
(
Point3::new(cx, cy, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
)
};
let segments = 24usize;
let mut points = Vec::with_capacity(segments + 1);
for i in 0..=segments {
let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
let p = center + x_axis * (radius * angle.cos()) + y_axis * (radius * angle.sin());
points.push(p);
}
Ok(points)
}
fn process_polyline_3d(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
let points = decoder.resolve_ref_list(points_attr)?;
let mut result = Vec::with_capacity(points.len());
for point in points {
let coords_attr = point
.get(0)
.ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
let coords = coords_attr
.as_list()
.ok_or_else(|| Error::geometry("Coordinates is not a list".to_string()))?;
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);
result.push(Point3::new(x, y, z));
}
Ok(result)
}
fn process_composite_curve_3d_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point3<f64>>> {
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segments = decoder.resolve_ref_list(segments_attr)?;
let mut result = Vec::new();
for segment in segments {
let parent_curve_attr = segment.get(2).ok_or_else(|| {
Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
})?;
let parent_curve = decoder
.resolve_ref(parent_curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
let same_sense = segment
.get(1)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
_ => None,
})
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let mut segment_points = self.get_curve_points_with_depth(&parent_curve, decoder, depth + 1)?;
if !same_sense {
segment_points.reverse();
}
if !result.is_empty() && !segment_points.is_empty() {
result.extend(segment_points.into_iter().skip(1));
} else {
result.extend(segment_points);
}
}
Ok(result)
}
fn process_trimmed_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
let basis_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("TrimmedCurve missing BasisCurve".to_string()))?;
let basis_curve = decoder
.resolve_ref(basis_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve BasisCurve".to_string()))?;
let trim1 = curve.get(1).and_then(|v| self.extract_trim_param(v));
let trim2 = curve.get(2).and_then(|v| self.extract_trim_param(v));
let sense = curve
.get(3)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T"),
_ => None,
})
.unwrap_or(true);
match basis_curve.ifc_type {
IfcType::IfcCircle | IfcType::IfcEllipse => {
self.process_trimmed_conic(&basis_curve, trim1, trim2, sense, decoder)
}
_ => {
self.process_curve_with_depth(&basis_curve, decoder, depth + 1)
}
}
}
fn extract_trim_param(&self, attr: &ifc_lite_core::AttributeValue) -> Option<f64> {
if let Some(list) = attr.as_list() {
for item in list {
if let Some(inner_list) = item.as_list() {
if inner_list.len() >= 2 {
if let Some(type_name) = inner_list.first().and_then(|v| v.as_string()) {
if type_name == "IFCPARAMETERVALUE" {
return inner_list.get(1).and_then(|v| v.as_float());
}
}
}
}
if let Some(f) = item.as_float() {
return Some(f);
}
}
}
None
}
fn process_trimmed_conic(
&self,
basis: &DecodedEntity,
trim1: Option<f64>,
trim2: Option<f64>,
sense: bool,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let radius = basis.get_float(1).unwrap_or(1.0);
let radius2 = if basis.ifc_type == IfcType::IfcEllipse {
basis.get_float(2).unwrap_or(radius)
} else {
radius
};
let (center, rotation) = self.get_placement_2d(basis, decoder)?;
let start_angle = trim1.unwrap_or(0.0).to_radians();
let mut end_angle = trim2.unwrap_or(360.0).to_radians();
if sense && end_angle < start_angle {
end_angle += 2.0 * std::f64::consts::PI;
} else if !sense && end_angle > start_angle {
end_angle -= 2.0 * std::f64::consts::PI;
}
let arc_angle = (end_angle - start_angle).abs();
let num_segments = ((arc_angle / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize).max(2);
let mut points = Vec::with_capacity(num_segments + 1);
let angle_range = if sense {
end_angle - start_angle
} else {
start_angle - end_angle
};
for i in 0..=num_segments {
let t = i as f64 / num_segments as f64;
let angle = if sense {
start_angle + t * angle_range
} else {
start_angle - t * angle_range.abs()
};
let x = radius * angle.cos();
let y = radius2 * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
fn get_placement_2d(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Point2<f64>, f64)> {
let placement_attr = match entity.get(0) {
Some(attr) if !attr.is_null() => attr,
_ => return Ok((Point2::new(0.0, 0.0), 0.0)),
};
let placement = match decoder.resolve_ref(placement_attr)? {
Some(p) => p,
None => return Ok((Point2::new(0.0, 0.0), 0.0)),
};
let location_attr = placement.get(0);
let center = if let Some(loc_attr) = location_attr {
if let Some(loc) = decoder.resolve_ref(loc_attr)? {
let coords = loc.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
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);
Point2::new(x, y)
} else {
Point2::new(0.0, 0.0)
}
} else {
Point2::new(0.0, 0.0)
}
} else {
Point2::new(0.0, 0.0)
};
let rotation = if let Some(dir_attr) = placement.get(1) {
if let Some(dir) = decoder.resolve_ref(dir_attr)? {
let ratios = dir.get(0).and_then(|v| v.as_list());
if let Some(ratios) = ratios {
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
y.atan2(x)
} else {
0.0
}
} else {
0.0
}
} else {
0.0
};
Ok((center, rotation))
}
fn process_circle_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let radius = curve.get_float(1).unwrap_or(1.0);
let (center, rotation) = self.get_placement_2d(curve, decoder)?;
let segments = 36;
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
fn process_ellipse_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let semi_axis1 = curve.get_float(1).unwrap_or(1.0);
let semi_axis2 = curve.get_float(2).unwrap_or(1.0);
let (center, rotation) = self.get_placement_2d(curve, decoder)?;
let segments = 36;
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = semi_axis1 * angle.cos();
let y = semi_axis2 * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
#[inline]
fn process_polyline(
&self,
polyline: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let points_attr = polyline
.get(0)
.ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
let point_entities = decoder.resolve_ref_list(points_attr)?;
let mut points = Vec::with_capacity(point_entities.len());
for point_entity in point_entities {
if point_entity.ifc_type != IfcType::IfcCartesianPoint {
continue;
}
let coords_attr = point_entity
.get(0)
.ok_or_else(|| Error::geometry("CartesianPoint missing coordinates".to_string()))?;
let coords = coords_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
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);
points.push(Point2::new(x, y));
}
Ok(points)
}
fn process_indexed_polycurve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("IndexedPolyCurve missing Points".to_string()))?;
let points_list = decoder
.resolve_ref(points_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve Points list".to_string()))?;
let coord_list_attr = points_list
.get(0)
.ok_or_else(|| Error::geometry("CartesianPointList2D missing CoordList".to_string()))?;
let coord_list = coord_list_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
let all_points: Vec<Point2<f64>> = coord_list
.iter()
.filter_map(|coord| {
coord.as_list().and_then(|coords| {
let x = coords.first()?.as_float()?;
let y = coords.get(1)?.as_float()?;
Some(Point2::new(x, y))
})
})
.collect();
let segments_attr = curve.get(1);
if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
return Ok(all_points);
}
let segments = segments_attr
.unwrap()
.as_list()
.ok_or_else(|| Error::geometry("Expected segments list".to_string()))?;
let mut result_points = Vec::new();
for segment in segments {
let (is_arc, indices) = if let Some(segment_list) = segment.as_list() {
if segment_list.len() >= 2 {
let type_name = segment_list.first()
.and_then(|v| v.as_string())
.unwrap_or("");
let is_arc_type = type_name.to_uppercase().contains("ARC");
if let Some(AttributeValue::List(indices_list)) = segment_list.get(1) {
(is_arc_type, Some(indices_list.as_slice()))
} else {
(false, Some(segment_list))
}
} else {
(false, Some(segment_list))
}
} else {
(false, None)
};
if let Some(indices) = indices {
let idx_values: Vec<usize> = indices
.iter()
.filter_map(|v| v.as_float().map(|f| f as usize - 1)) .collect();
if is_arc && idx_values.len() == 3 {
let p1 = all_points.get(idx_values[0]).copied();
let p2 = all_points.get(idx_values[1]).copied(); let p3 = all_points.get(idx_values[2]).copied();
if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
let chord_len =
((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
let mid_chord = ((mid.x - (start.x + end.x) / 2.0).powi(2)
+ (mid.y - (start.y + end.y) / 2.0).powi(2))
.sqrt();
let arc_estimate = if chord_len > 1e-10 {
(mid_chord / chord_len).abs().min(1.0).acos() * 2.0
} else {
0.5
};
let num_segments = ((arc_estimate / std::f64::consts::FRAC_PI_2 * 8.0)
.ceil() as usize)
.clamp(4, 16);
let arc_points = self.approximate_arc_3pt(start, mid, end, num_segments);
for pt in arc_points {
if result_points.last() != Some(&pt) {
result_points.push(pt);
}
}
}
} else {
for &idx in &idx_values {
if let Some(&pt) = all_points.get(idx) {
if result_points.last() != Some(&pt) {
result_points.push(pt);
}
}
}
}
}
}
Ok(result_points)
}
fn approximate_arc_3pt(
&self,
p1: Point2<f64>,
p2: Point2<f64>,
p3: Point2<f64>,
num_segments: usize,
) -> Vec<Point2<f64>> {
let ax = p1.x;
let ay = p1.y;
let bx = p2.x;
let by = p2.y;
let cx = p3.x;
let cy = p3.y;
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
let arc_span = ((p3.x - p1.x).powi(2) + (p3.y - p1.y).powi(2)).sqrt();
let collinear_tolerance = 1e-6 * arc_span.powi(2).max(1e-10);
if d.abs() < collinear_tolerance {
return vec![p1, p2, p3];
}
let ux_num = (ax * ax + ay * ay) * (by - cy)
+ (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by);
let uy_num = (ax * ax + ay * ay) * (cx - bx)
+ (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax);
let ux = ux_num / d;
let uy = uy_num / d;
let center = Point2::new(ux, uy);
let radius = ((p1.x - center.x).powi(2) + (p1.y - center.y).powi(2)).sqrt();
if radius > arc_span * 100.0 {
return vec![p1, p2, p3];
}
let angle1 = (p1.y - center.y).atan2(p1.x - center.x);
let angle3 = (p3.y - center.y).atan2(p3.x - center.x);
let angle2 = (p2.y - center.y).atan2(p2.x - center.x);
fn normalize_angle(a: f64) -> f64 {
let mut a = a % (2.0 * PI);
if a > PI {
a -= 2.0 * PI;
} else if a < -PI {
a += 2.0 * PI;
}
a
}
let diff_direct = normalize_angle(angle3 - angle1);
let diff_to_mid = normalize_angle(angle2 - angle1);
let go_direct = if diff_direct > 0.0 {
diff_to_mid > 0.0 && diff_to_mid < diff_direct
} else {
diff_to_mid < 0.0 && diff_to_mid > diff_direct
};
let start_angle = angle1;
let end_angle = if go_direct {
angle1 + diff_direct
} else {
if diff_direct > 0.0 {
angle1 + diff_direct - 2.0 * PI
} else {
angle1 + diff_direct + 2.0 * PI
}
};
let mut points = Vec::with_capacity(num_segments + 1);
for i in 0..=num_segments {
let t = i as f64 / num_segments as f64;
let angle = start_angle + t * (end_angle - start_angle);
points.push(Point2::new(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
));
}
points
}
fn process_composite_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segments = decoder.resolve_ref_list(segments_attr)?;
let mut all_points = Vec::new();
for segment in segments {
if segment.ifc_type != IfcType::IfcCompositeCurveSegment {
continue;
}
let parent_curve_attr = segment.get(2).ok_or_else(|| {
Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
})?;
let parent_curve = decoder
.resolve_ref(parent_curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
let same_sense = segment
.get(1)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T" || s == "TRUE"),
_ => None,
})
.unwrap_or(true);
let mut segment_points = self.process_curve_with_depth(&parent_curve, decoder, depth + 1)?;
if !same_sense {
segment_points.reverse();
}
for pt in segment_points {
if all_points.last() != Some(&pt) {
all_points.push(pt);
}
}
}
Ok(all_points)
}
fn process_composite(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let profiles_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Composite profile missing Profiles".to_string()))?;
let sub_profiles = decoder.resolve_ref_list(profiles_attr)?;
if sub_profiles.is_empty() {
return Err(Error::geometry(
"Composite profile has no sub-profiles".to_string(),
));
}
let mut result = self.process(&sub_profiles[0], decoder)?;
for sub_profile in &sub_profiles[1..] {
let hole = self.process(sub_profile, decoder)?;
result.add_hole(hole.outer);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_profile() {
let content = r#"
#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(!profile.outer.is_empty());
}
#[test]
fn test_circle_profile() {
let content = r#"
#1=IFCCIRCLEPROFILEDEF(.AREA.,$,$,50.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 36); assert!(!profile.outer.is_empty());
}
#[test]
fn test_i_shape_profile() {
let content = r#"
#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,200.0,300.0,10.0,15.0,$,$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 12); assert!(!profile.outer.is_empty());
}
#[test]
fn test_arbitrary_profile() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0));
#2=IFCCARTESIANPOINT((100.0,0.0));
#3=IFCCARTESIANPOINT((100.0,100.0));
#4=IFCCARTESIANPOINT((0.0,100.0));
#5=IFCPOLYLINE((#1,#2,#3,#4,#1));
#6=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#5);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 5); assert!(!profile.outer.is_empty());
}
#[test]
fn test_derived_profile_applies_translation_rotation_and_scale() {
let content = r#"
#1=IFCDIRECTION((0.0,1.0));
#2=IFCCARTESIANPOINT((10.0,20.0));
#3=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,$,#2,2.0);
#4=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#5=IFCDERIVEDPROFILEDEF(.AREA.,$,#4,#3,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(5).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(14.0, 18.0)));
assert!(profile.outer.contains(&Point2::new(14.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 18.0)));
}
#[test]
fn test_mirrored_profile_uses_derived_operator() {
let content = r#"
#1=IFCDIRECTION((-1.0,0.0));
#2=IFCDIRECTION((0.0,1.0));
#3=IFCCARTESIANPOINT((0.0,0.0));
#4=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,#2,#3,1.0);
#5=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#6=IFCMIRROREDPROFILEDEF(.AREA.,$,#5,#4,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor.process(&profile_entity, &mut decoder).unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, 2.0)));
assert!(profile.outer.contains(&Point2::new(1.0, 2.0)));
}
}