use crate::profile::Profile2D;
use crate::tessellation::TessellationQuality;
use crate::{Error, Point2, Point3, Result};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType, ProfileCategory};
use std::cell::Cell;
mod curves_2d;
mod curves_3d;
mod outline;
mod placement;
mod shapes;
mod steel_shapes;
mod simplify;
#[cfg(test)]
mod tests;
use outline::trim_polyline;
use simplify::{mirror_profile_about_y_axis, simplify_smooth_curve_polyline};
const MAX_CURVE_DEPTH: u32 = 50;
const MAX_CURVE_NODES: u32 = 100_000;
#[derive(Debug, Clone, Copy)]
enum TrimSelect {
Parameter(f64),
Cartesian(Point2<f64>),
}
const MAX_PROFILE_DEPTH: u32 = 16;
pub struct ProfileProcessor {
schema: IfcSchema,
active_quality: Cell<TessellationQuality>,
curve_budget: Cell<u32>,
}
impl ProfileProcessor {
pub fn new(schema: IfcSchema) -> Self {
Self {
schema,
active_quality: Cell::new(TessellationQuality::Medium),
curve_budget: Cell::new(MAX_CURVE_NODES),
}
}
#[inline]
fn quality(&self) -> TessellationQuality {
self.active_quality.get()
}
fn spend_curve_node(&self) -> Result<()> {
match self.curve_budget.get().checked_sub(1) {
Some(left) => {
self.curve_budget.set(left);
Ok(())
}
None => Err(Error::geometry(format!(
"Curve traversal exceeded {MAX_CURVE_NODES} nested curves"
))),
}
}
#[inline]
pub fn set_tessellation_quality(&self, quality: TessellationQuality) {
self.active_quality.set(quality);
self.curve_budget.set(MAX_CURVE_NODES);
}
#[inline]
pub fn process(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
quality: TessellationQuality,
) -> Result<Profile2D> {
self.active_quality.set(quality);
self.curve_budget.set(MAX_CURVE_NODES);
self.process_with_depth(profile, decoder, 0)
}
fn process_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Profile2D> {
if depth > MAX_PROFILE_DEPTH {
return Err(Error::geometry(format!(
"Profile nesting depth {} exceeds limit {} at #{}",
depth, MAX_PROFILE_DEPTH, profile.id
)));
}
match profile.ifc_type {
IfcType::IfcDerivedProfileDef | IfcType::IfcMirroredProfileDef => {
self.process_derived_with_depth(profile, decoder, depth)
}
_ => 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_with_depth(profile, decoder, depth),
_ => 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::IfcRoundedRectangleProfileDef => self.process_rounded_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::IfcAsymmetricIShapeProfileDef => self.process_asymmetric_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
))),
}?;
base_profile.center_on_bbox();
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 process_derived_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> 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_with_depth(&parent_profile, decoder, depth + 1)?;
if profile.ifc_type == IfcType::IfcMirroredProfileDef {
mirror_profile_about_y_axis(&mut result);
return Ok(result);
}
let Some(operator_attr) = profile.get(3) else {
return Ok(result);
};
if operator_attr.is_null() {
return Ok(result);
}
let Some(operator) = decoder.resolve_ref(operator_attr)? else {
return Ok(result);
};
self.apply_cartesian_transformation_operator_2d(&mut result, &operator, decoder)?;
Ok(result)
}
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 raw_outer = self.process_curve(&curve, decoder)?;
let outer_points = simplify_smooth_curve_polyline(&raw_outer, decoder.length_unit_scale());
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 raw_hole = self.process_curve(&inner_curve, decoder)?;
let hole_points =
simplify_smooth_curve_polyline(&raw_hole, decoder.length_unit_scale());
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
)));
}
self.spend_curve_node()?;
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),
IfcType::IfcLine => Ok(self
.get_line_points_3d(curve, decoder, 0.0, 1.0)?
.into_iter()
.map(|p| Point2::new(p.x, p.y))
.collect()),
_ => Err(Error::geometry(format!(
"Unsupported curve type: {}",
curve.ifc_type
))),
}
}
#[inline]
pub fn get_curve_points(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
quality: TessellationQuality,
) -> Result<Vec<Point3<f64>>> {
self.active_quality.set(quality);
self.curve_budget.set(MAX_CURVE_NODES);
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
)));
}
self.spend_curve_node()?;
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::IfcGradientCurve => {
if let Some(base_attr) = curve.get(2) {
if !base_attr.is_null() {
if let Some(base) = decoder.resolve_ref(base_attr)? {
return self.get_curve_points_with_depth(&base, decoder, depth + 1);
}
}
}
self.process_composite_curve_3d_with_depth(curve, decoder, depth)
}
IfcType::IfcCircle => self.process_circle_3d(curve, decoder),
IfcType::IfcIndexedPolyCurve => {
self.process_indexed_polycurve_3d(curve, decoder)
}
IfcType::IfcLine => self.get_line_points_3d(curve, decoder, 0.0, 1.0),
IfcType::IfcTrimmedCurve => {
if let Some(basis_attr) = curve.get(0) {
if let Some(basis) = decoder.resolve_ref(basis_attr)? {
match basis.ifc_type {
IfcType::IfcLine => {
return self.process_trimmed_line_3d(curve, &basis, decoder);
}
IfcType::IfcCircle | IfcType::IfcEllipse => {
return self.process_trimmed_conic_3d(curve, &basis, decoder);
}
_ => {}
}
}
}
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_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();
let mut last_curve_segment_terminal: Option<Point3<f64>> = None;
for segment in segments {
if segment.ifc_type == IfcType::IfcCurveSegment {
if let Some(placement_attr) = segment.get(1) {
if !placement_attr.is_null() {
if let Some(placement) = decoder.resolve_ref(placement_attr)? {
if let Some((origin, x_axis)) =
axis2_placement_location_and_x_axis_3d(&placement, decoder)
{
if result.last().is_none_or(|last: &Point3<f64>| {
(last - origin).norm() > 1e-9
}) {
result.push(origin);
}
let segment_length = segment
.get(3)
.and_then(|a| a.as_float())
.unwrap_or(0.0);
if segment_length > 1e-9 {
last_curve_segment_terminal =
Some(origin + x_axis * segment_length);
} else {
last_curve_segment_terminal = None;
}
continue;
}
}
}
}
last_curve_segment_terminal = None;
continue;
}
last_curve_segment_terminal = None;
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);
}
}
if let Some(terminal) = last_curve_segment_terminal {
if result.last().is_none_or(|last: &Point3<f64>| {
(last - terminal).norm() > 1e-9
}) {
result.push(terminal);
}
}
Ok(result)
}
pub fn get_composite_curve_points_trimmed(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
start_param: Option<f64>,
end_param: Option<f64>,
) -> 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 num_segments = segments.len();
if num_segments == 0 {
return Ok(Vec::new());
}
let start = start_param.unwrap_or(0.0).max(0.0);
let end = end_param.unwrap_or(num_segments as f64).min(num_segments as f64);
if end <= start {
return Ok(Vec::new());
}
let mut result: Vec<Point3<f64>> = Vec::new();
for (idx, segment) in segments.into_iter().enumerate() {
let seg_start = idx as f64;
let seg_end = seg_start + 1.0;
if seg_end <= start || seg_start >= end {
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(e) => Some(e.as_str()),
_ => None,
})
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let mut seg_points = self.get_curve_points_with_depth(&parent_curve, decoder, 1)?;
if !same_sense {
seg_points.reverse();
}
if seg_points.len() < 2 {
continue;
}
let local_start = (start - seg_start).clamp(0.0, 1.0);
let local_end = (end - seg_start).clamp(0.0, 1.0);
if local_end <= local_start {
continue;
}
let trimmed = if local_start == 0.0 && local_end == 1.0 {
seg_points
} else {
trim_polyline(&seg_points, local_start, local_end)
};
if trimmed.is_empty() {
continue;
}
const JUNCTION_EPS: f64 = 1e-6;
let mut iter = trimmed.into_iter();
if let Some(first) = iter.next() {
let coincident = result.last().is_some_and(|last| {
(first.x - last.x).abs() < JUNCTION_EPS
&& (first.y - last.y).abs() < JUNCTION_EPS
&& (first.z - last.z).abs() < JUNCTION_EPS
});
if !coincident {
result.push(first);
}
result.extend(iter);
}
}
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 prefer_cartesian = curve
.get(4)
.and_then(|v| v.as_enum())
.map(|m| m == "CARTESIAN")
.unwrap_or(false);
let trim1 = curve
.get(1)
.and_then(|v| self.extract_trim_select(v, prefer_cartesian, decoder));
let trim2 = curve
.get(2)
.and_then(|v| self.extract_trim_select(v, prefer_cartesian, decoder));
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)
}
IfcType::IfcLine => {
Ok(self
.process_trimmed_line_3d(curve, &basis_curve, decoder)?
.into_iter()
.map(|p| Point2::new(p.x, p.y))
.collect())
}
_ => {
self.process_curve_with_depth(&basis_curve, decoder, depth + 1)
}
}
}
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_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> 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_with_depth(&sub_profiles[0], decoder, depth + 1)?;
for sub_profile in &sub_profiles[1..] {
let hole = self.process_with_depth(sub_profile, decoder, depth + 1)?;
result.add_hole(hole.outer);
}
Ok(result)
}
}
fn axis2_placement_location_and_x_axis_3d(
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<(Point3<f64>, nalgebra::Vector3<f64>)> {
let is_3d = placement.ifc_type == IfcType::IfcAxis2Placement3D;
let is_2d = placement.ifc_type == IfcType::IfcAxis2Placement2D;
if !is_2d && !is_3d {
return None;
}
let location_attr = placement.get(0)?;
if location_attr.is_null() {
return None;
}
let location = decoder.resolve_ref(location_attr).ok().flatten()?;
if location.ifc_type != IfcType::IfcCartesianPoint {
return None;
}
let coords = location.get(0)?.as_list()?;
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);
let origin = Point3::new(x, y, z);
let ref_dir_idx = if is_3d { 2 } else { 1 };
let mut x_axis = nalgebra::Vector3::x();
if let Some(dir_attr) = placement.get(ref_dir_idx) {
if !dir_attr.is_null() {
if let Some(dir) = decoder.resolve_ref(dir_attr).ok().flatten() {
if dir.ifc_type == IfcType::IfcDirection {
if let Some(ratios) = dir.get(0).and_then(|a| a.as_list()) {
let dx = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let dy = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let dz = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
let v = nalgebra::Vector3::new(dx, dy, dz);
if v.norm() > 1e-12 {
x_axis = v.normalize();
}
}
}
}
}
}
Some((origin, x_axis))
}
#[cfg(test)]
#[path = "curve_fanout_tests.rs"]
mod curve_fanout_tests;