use axiolid_core::{Interval, Scalar, Transform2, Vec2};
use axiolid_curve::{Curve2, Line2};
use axiolid_model::{GeometryNode, NodeId};
use axiolid_profile::CenterLineProfile;
use axiolid_profile::{
CircleProfile, Contour, ContourProfile, EllipseProfile, Profile, ProfileSegment,
RectangleProfile, SectionProfile,
};
use ifc_model::{EntityId, Model};
use crate::error::{GeometryError, GeometryResult};
use crate::lower::session::LoweringSession;
use crate::resource::operator::operator_transform;
use crate::slots::Slots;
use crate::transform::Transform;
use crate::units::UnitScale;
mod composite;
mod open;
pub use open::lower_open_profile_node;
pub const IMPLEMENTED_PROFILES: &[&str] = &[
"IFCARBITRARYCLOSEDPROFILEDEF",
"IFCARBITRARYOPENPROFILEDEF",
"IFCARBITRARYPROFILEDEFWITHVOIDS",
"IFCASYMMETRICISHAPEPROFILEDEF",
"IFCCENTERLINEPROFILEDEF",
"IFCCIRCLEHOLLOWPROFILEDEF",
"IFCCIRCLEPROFILEDEF",
"IFCCOMPOSITEPROFILEDEF",
"IFCCSHAPEPROFILEDEF",
"IFCDERIVEDPROFILEDEF",
"IFCELLIPSEPROFILEDEF",
"IFCISHAPEPROFILEDEF",
"IFCLSHAPEPROFILEDEF",
"IFCMIRROREDPROFILEDEF",
"IFCRECTANGLEHOLLOWPROFILEDEF",
"IFCRECTANGLEPROFILEDEF",
"IFCROUNDEDRECTANGLEPROFILEDEF",
"IFCTSHAPEPROFILEDEF",
"IFCTRAPEZIUMPROFILEDEF",
"IFCUSHAPEPROFILEDEF",
"IFCZSHAPEPROFILEDEF",
];
pub const PLANNED_PROFILES: &[(&str, &str)] = &[(
"IFCPROFILEDEF",
"generic profile declaration carries no concrete geometry to lower",
)];
pub(crate) use crate::slots::profile_slot as slot;
use crate::slots::section_slot;
pub const UNLOWERED: &[(&str, &str)] = &[];
const PROFILE: &str = "profile";
const OPEN_PROFILE: &str = "open-profile";
pub fn lower_profile_node(
session: &mut LoweringSession<'_>,
id: EntityId,
) -> GeometryResult<NodeId> {
let frame = crate::transform::Transform::identity();
if let Some(node) = session.memoized(id, PROFILE, frame) {
return Ok(node);
}
let profile = lower_profile(session.model(), id, session.units())?;
let node = session.node_for(id, GeometryNode::Profile(profile))?;
session.memoize(id, PROFILE, frame, node);
Ok(node)
}
pub fn lower_profile(model: &Model, id: EntityId, units: &UnitScale) -> GeometryResult<Profile> {
lower_profile_depth(model, id, units, 0)
}
const MAX_PROFILE_DEPTH: usize = 16;
fn lower_profile_depth(
model: &Model,
id: EntityId,
units: &UnitScale,
depth: usize,
) -> GeometryResult<Profile> {
if depth > MAX_PROFILE_DEPTH {
return Err(GeometryError::Unsupported {
entity: id,
type_name: "IFCPROFILEDEF".to_string(),
detail: "profile nesting exceeded the depth budget",
});
}
let entity = model.get(id).ok_or(GeometryError::MissingEntity {
referrer: id,
missing: id,
})?;
let slots = Slots::new(id, entity);
let type_name = entity.type_name.to_ascii_uppercase();
let profile = match type_name.as_str() {
"IFCRECTANGLEPROFILEDEF" => rectangle(&slots, units, None)?,
"IFCROUNDEDRECTANGLEPROFILEDEF" => {
let radius = units.length(slots.req_f64(slot::ROUNDED_RECT_RADIUS, "RoundingRadius")?);
rectangle(&slots, units, Some(radius))?
}
"IFCRECTANGLEHOLLOWPROFILEDEF" => rectangle_hollow(&slots, units)?,
"IFCCIRCLEPROFILEDEF" => circle(&slots, units, None)?,
"IFCCIRCLEHOLLOWPROFILEDEF" => circle_hollow(&slots, units)?,
"IFCARBITRARYCLOSEDPROFILEDEF" => arbitrary(model, &slots, units, false)?,
"IFCARBITRARYPROFILEDEFWITHVOIDS" => arbitrary(model, &slots, units, true)?,
"IFCISHAPEPROFILEDEF" => i_shape(&slots, units)?,
"IFCASYMMETRICISHAPEPROFILEDEF" => asymmetric_i(&slots, units)?,
"IFCLSHAPEPROFILEDEF" => l_shape(&slots, units)?,
"IFCTSHAPEPROFILEDEF" => t_shape(&slots, units)?,
"IFCUSHAPEPROFILEDEF" => u_shape(&slots, units)?,
"IFCCSHAPEPROFILEDEF" => c_shape(&slots, units)?,
"IFCZSHAPEPROFILEDEF" => z_shape(&slots, units)?,
"IFCTRAPEZIUMPROFILEDEF" => trapezium(&slots, units)?,
"IFCELLIPSEPROFILEDEF" => ellipse(&slots, units)?,
"IFCCOMPOSITEPROFILEDEF" => composite(model, &slots, units, depth)?,
"IFCDERIVEDPROFILEDEF" => derived(model, id, &slots, units, depth, false)?,
"IFCMIRROREDPROFILEDEF" => derived(model, id, &slots, units, depth, true)?,
"IFCCENTERLINEPROFILEDEF" => center_line(model, &slots, units)?,
"IFCARBITRARYOPENPROFILEDEF" => {
return Err(GeometryError::Unsupported {
entity: id,
type_name: type_name.to_string(),
detail: "open profiles have no area; use lower_open_profile_node",
});
}
"IFCPROFILEDEF" => {
return Err(GeometryError::Unsupported {
entity: id,
type_name: type_name.to_string(),
detail: PLANNED_PROFILES[0].1,
});
}
other => {
let detail = UNLOWERED
.iter()
.find(|(name, _)| *name == other)
.map(|(_, reason)| *reason)
.unwrap_or("profile subtype is not lowered yet");
return Err(GeometryError::Unsupported {
entity: id,
type_name: other.to_string(),
detail,
});
}
};
if type_name.contains("RECTANGLE") || type_name.contains("CIRCLE") {
apply_parameterized_position(model, &slots, units, profile)
} else {
Ok(profile)
}
}
fn rectangle(
slots: &Slots<'_>,
units: &UnitScale,
outer_radius: Option<f64>,
) -> GeometryResult<Profile> {
let x = units.length(slots.req_f64(slot::X_DIM, "XDim")?);
let y = units.length(slots.req_f64(slot::Y_DIM, "YDim")?);
if x <= 0.0 || y <= 0.0 || outer_radius.is_some_and(|radius| radius < 0.0) {
return Err(slots.degenerate("rectangle dimensions and radius must be non-negative"));
}
Ok(Profile::Rectangle(RectangleProfile {
x,
y,
thickness: None,
outer_radius,
inner_radius: None,
}))
}
fn rectangle_hollow(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
let x = units.length(slots.req_f64(slot::X_DIM, "XDim")?);
let y = units.length(slots.req_f64(slot::Y_DIM, "YDim")?);
let thickness = units.length(slots.req_f64(slot::RECT_WALL_THICKNESS, "WallThickness")?);
if x <= 0.0 || y <= 0.0 || thickness <= 0.0 || 2.0 * thickness >= x.min(y) {
return Err(slots.degenerate("wall thickness consumes the rectangular section"));
}
Ok(Profile::Rectangle(RectangleProfile {
x,
y,
thickness: Some(thickness),
inner_radius: slots
.opt_f64(slot::RECT_INNER_RADIUS)
.map(|value| units.length(value)),
outer_radius: slots
.opt_f64(slot::RECT_OUTER_RADIUS)
.map(|value| units.length(value)),
}))
}
fn circle(slots: &Slots<'_>, units: &UnitScale, thickness: Option<f64>) -> GeometryResult<Profile> {
let radius = units.length(slots.req_f64(slot::RADIUS, "Radius")?);
if radius <= 0.0 || thickness.is_some_and(|wall| wall <= 0.0 || wall >= radius) {
return Err(slots.degenerate("circle radius or wall thickness is non-physical"));
}
Ok(Profile::Circle(CircleProfile { radius, thickness }))
}
fn circle_hollow(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
let thickness = units.length(slots.req_f64(slot::CIRCLE_WALL_THICKNESS, "WallThickness")?);
circle(slots, units, Some(thickness))
}
fn arbitrary(
model: &Model,
slots: &Slots<'_>,
units: &UnitScale,
with_voids: bool,
) -> GeometryResult<Profile> {
let outer = curve_to_contour(
model,
slots.req_ref(slot::OUTER_CURVE, "OuterCurve")?,
units,
)?;
let mut holes = Vec::new();
if with_voids {
for curve in slots.req_ref_list(slot::INNER_CURVES, "InnerCurves")? {
holes.push(curve_to_contour(model, curve, units)?);
}
}
Ok(Profile::Contour(ContourProfile { outer, holes }))
}
fn curve_to_contour(model: &Model, id: EntityId, units: &UnitScale) -> GeometryResult<Contour> {
let entity = model.get(id).ok_or(GeometryError::MissingEntity {
referrer: id,
missing: id,
})?;
let type_name = entity.type_name.to_ascii_uppercase();
match type_name.as_str() {
"IFCPOLYLINE" => {}
"IFCCOMPOSITECURVE" => return composite::composite_contour(model, id, units),
_ => {
return Err(GeometryError::Unsupported {
entity: id,
type_name,
detail: "profile boundaries lower IfcPolyline and IfcCompositeCurve only",
})
}
}
let slots = Slots::new(id, entity);
let mut points = polyline_points(model, id, units)?;
drop_closing_duplicate(&mut points);
if points.len() < 3 {
return Err(slots.degenerate("profile boundary has fewer than 3 distinct points"));
}
let segments = (0..points.len())
.map(|index| {
let origin = points[index];
let next = points[(index + 1) % points.len()];
ProfileSegment {
curve: Curve2::Line(Line2 {
origin,
direction: next - origin,
}),
domain: Interval::UNIT,
same_sense: true,
}
})
.collect();
Ok(Contour::new(segments))
}
fn polyline_points(model: &Model, id: EntityId, units: &UnitScale) -> GeometryResult<Vec<Vec2>> {
let entity = model.get(id).ok_or(GeometryError::MissingEntity {
referrer: id,
missing: id,
})?;
let slots = Slots::new(id, entity);
let mut points = Vec::new();
for point_id in slots.req_ref_list(0, "Points")? {
let point = model.get(point_id).ok_or(GeometryError::MissingEntity {
referrer: id,
missing: point_id,
})?;
let coordinates = Slots::new(point_id, point).req_f64_list(0, "Coordinates")?;
if coordinates.len() < 2 {
return Err(GeometryError::Degenerate {
entity: point_id,
type_name: point.type_name.to_string(),
detail: "profile boundary point is not at least 2D".to_string(),
});
}
points.push(Vec2::new(
units.length(coordinates[0]),
units.length(coordinates[1]),
));
}
Ok(points)
}
fn drop_closing_duplicate(points: &mut Vec<Vec2>) {
if points.len() >= 2 && points[0].distance(*points.last().expect("length checked")) < 1e-12 {
points.pop();
}
}
fn apply_parameterized_position(
model: &Model,
slots: &Slots<'_>,
units: &UnitScale,
profile: Profile,
) -> GeometryResult<Profile> {
let Some(position_id) = slots.opt_ref(slot::POSITION) else {
return Ok(profile);
};
let position = model.get(position_id).ok_or(GeometryError::MissingEntity {
referrer: slots.id(),
missing: position_id,
})?;
let position_slots = Slots::new(position_id, position);
let location_id = position_slots.req_ref(0, "Location")?;
let location = model.get(location_id).ok_or(GeometryError::MissingEntity {
referrer: position_id,
missing: location_id,
})?;
let coordinates = Slots::new(location_id, location).req_f64_list(0, "Coordinates")?;
if coordinates.len() < 2 {
return Err(position_slots.degenerate("2D placement location is not 2D"));
}
let origin = Vec2::new(units.length(coordinates[0]), units.length(coordinates[1]));
let x = if let Some(direction_id) = position_slots.opt_ref(1) {
let direction = model
.get(direction_id)
.ok_or(GeometryError::MissingEntity {
referrer: position_id,
missing: direction_id,
})?;
let ratios = Slots::new(direction_id, direction).req_f64_list(0, "DirectionRatios")?;
if ratios.len() < 2 {
return Err(position_slots.degenerate("2D reference direction is not 2D"));
}
Vec2::new(ratios[0], ratios[1])
.try_normalize()
.ok_or_else(|| position_slots.degenerate("2D reference direction has zero length"))?
} else {
Vec2::X
};
let y = Vec2::new(-x.y, x.x);
Ok(Profile::Derived {
basis: Box::new(profile),
transform: Transform2::from_cols(x, y, origin),
})
}
fn opt_len(slots: &Slots<'_>, slot: usize, units: &UnitScale) -> Option<Scalar> {
slots.opt_f64(slot).map(|v| units.length(v))
}
fn opt_angle(slots: &Slots<'_>, slot: usize, units: &UnitScale) -> Option<Scalar> {
slots.opt_f64(slot).map(|v| units.angle(v))
}
fn i_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::I {
depth: units.length(slots.req_f64(section_slot::I_DEPTH, "OverallDepth")?),
width: units.length(slots.req_f64(section_slot::I_WIDTH, "OverallWidth")?),
web_thickness: units.length(slots.req_f64(section_slot::I_WEB, "WebThickness")?),
flange_thickness: units.length(slots.req_f64(section_slot::I_FLANGE, "FlangeThickness")?),
fillet_radius: opt_len(slots, section_slot::I_FILLET, units),
flange_edge_radius: opt_len(slots, section_slot::I_EDGE, units),
flange_slope: opt_angle(slots, section_slot::I_SLOPE, units),
}))
}
fn asymmetric_i(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
let bottom_thickness =
units.length(slots.req_f64(section_slot::AI_BOTTOM_FLANGE, "BottomFlangeThickness")?);
Ok(Profile::Section(SectionProfile::AsymmetricI {
depth: units.length(slots.req_f64(section_slot::AI_DEPTH, "OverallDepth")?),
web_thickness: units.length(slots.req_f64(section_slot::AI_WEB, "WebThickness")?),
bottom_flange_width: units
.length(slots.req_f64(section_slot::AI_BOTTOM_WIDTH, "BottomFlangeWidth")?),
bottom_flange_thickness: bottom_thickness,
bottom_fillet_radius: opt_len(slots, section_slot::AI_BOTTOM_FILLET, units),
bottom_flange_edge_radius: opt_len(slots, section_slot::AI_BOTTOM_EDGE, units),
bottom_flange_slope: opt_angle(slots, section_slot::AI_BOTTOM_SLOPE, units),
top_flange_width: units
.length(slots.req_f64(section_slot::AI_TOP_WIDTH, "TopFlangeWidth")?),
top_flange_thickness: opt_len(slots, section_slot::AI_TOP_FLANGE, units),
top_fillet_radius: opt_len(slots, section_slot::AI_TOP_FILLET, units),
top_flange_edge_radius: opt_len(slots, section_slot::AI_TOP_EDGE, units),
top_flange_slope: opt_angle(slots, section_slot::AI_TOP_SLOPE, units),
}))
}
fn l_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
let depth = units.length(slots.req_f64(section_slot::L_DEPTH, "Depth")?);
Ok(Profile::Section(SectionProfile::L {
depth,
width: opt_len(slots, section_slot::L_WIDTH, units),
thickness: units.length(slots.req_f64(section_slot::L_THICKNESS, "Thickness")?),
fillet_radius: opt_len(slots, section_slot::L_FILLET, units),
edge_radius: opt_len(slots, section_slot::L_EDGE, units),
leg_slope: opt_angle(slots, section_slot::L_SLOPE, units),
}))
}
fn t_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::T {
depth: units.length(slots.req_f64(section_slot::T_DEPTH, "Depth")?),
flange_width: units.length(slots.req_f64(section_slot::T_WIDTH, "FlangeWidth")?),
web_thickness: units.length(slots.req_f64(section_slot::T_WEB, "WebThickness")?),
flange_thickness: units.length(slots.req_f64(section_slot::T_FLANGE, "FlangeThickness")?),
fillet_radius: opt_len(slots, section_slot::T_FILLET, units),
flange_edge_radius: opt_len(slots, section_slot::T_FLANGE_EDGE, units),
web_edge_radius: opt_len(slots, section_slot::T_WEB_EDGE, units),
web_slope: opt_angle(slots, section_slot::T_WEB_SLOPE, units),
flange_slope: opt_angle(slots, section_slot::T_FLANGE_SLOPE, units),
}))
}
fn u_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::U {
depth: units.length(slots.req_f64(section_slot::U_DEPTH, "Depth")?),
flange_width: units.length(slots.req_f64(section_slot::U_WIDTH, "FlangeWidth")?),
web_thickness: units.length(slots.req_f64(section_slot::U_WEB, "WebThickness")?),
flange_thickness: units.length(slots.req_f64(section_slot::U_FLANGE, "FlangeThickness")?),
fillet_radius: opt_len(slots, section_slot::U_FILLET, units),
edge_radius: opt_len(slots, section_slot::U_EDGE, units),
flange_slope: opt_angle(slots, section_slot::U_SLOPE, units),
}))
}
fn c_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::C {
depth: units.length(slots.req_f64(section_slot::C_DEPTH, "Depth")?),
width: units.length(slots.req_f64(section_slot::C_WIDTH, "Width")?),
wall_thickness: units.length(slots.req_f64(section_slot::C_WALL, "WallThickness")?),
girth: units.length(slots.req_f64(section_slot::C_GIRTH, "Girth")?),
internal_fillet_radius: opt_len(slots, section_slot::C_FILLET, units),
}))
}
fn z_shape(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::Z {
depth: units.length(slots.req_f64(section_slot::Z_DEPTH, "Depth")?),
flange_width: units.length(slots.req_f64(section_slot::Z_FLANGE_WIDTH, "FlangeWidth")?),
web_thickness: units.length(slots.req_f64(section_slot::Z_WEB, "WebThickness")?),
flange_thickness: units.length(slots.req_f64(section_slot::Z_FLANGE, "FlangeThickness")?),
fillet_radius: opt_len(slots, section_slot::Z_FILLET, units),
edge_radius: opt_len(slots, section_slot::Z_EDGE, units),
}))
}
fn trapezium(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Section(SectionProfile::Trapezium {
bottom_x: units.length(slots.req_f64(section_slot::TZ_BOTTOM, "BottomXDim")?),
top_x: units.length(slots.req_f64(section_slot::TZ_TOP, "TopXDim")?),
y: units.length(slots.req_f64(section_slot::TZ_Y, "YDim")?),
top_offset: units.length(slots.req_f64(section_slot::TZ_OFFSET, "TopXOffset")?),
}))
}
fn ellipse(slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
Ok(Profile::Ellipse(EllipseProfile {
semi_axis_x: units.length(slots.req_f64(section_slot::E_SEMI1, "SemiAxis1")?),
semi_axis_y: units.length(slots.req_f64(section_slot::E_SEMI2, "SemiAxis2")?),
}))
}
fn composite(
model: &Model,
slots: &Slots<'_>,
units: &UnitScale,
depth: usize,
) -> GeometryResult<Profile> {
let refs = slots.req_ref_list(section_slot::COMPOSITE_PROFILES, "Profiles")?;
let mut members = Vec::with_capacity(refs.len());
for member in refs {
members.push(lower_profile_depth(model, member, units, depth + 1)?);
}
Ok(Profile::Composite(members))
}
fn derived(
model: &Model,
id: EntityId,
slots: &Slots<'_>,
units: &UnitScale,
depth: usize,
mirrored: bool,
) -> GeometryResult<Profile> {
let parent = slots.req_ref(section_slot::DERIVED_PARENT, "ParentProfile")?;
let basis = lower_profile_depth(model, parent, units, depth + 1)?;
let transform = (if mirrored {
Ok(Transform2::from_scale(Vec2::new(-1.0, 1.0)))
} else {
let operator = slots.req_ref(section_slot::DERIVED_OPERATOR, "Operator")?;
let op_entity = model.get(operator).ok_or(GeometryError::MissingEntity {
referrer: id,
missing: operator,
})?;
let full = operator_transform(model, operator, op_entity)?;
flatten_to_2d(&full)
})?;
Ok(Profile::Derived {
basis: Box::new(basis),
transform,
})
}
fn flatten_to_2d(full: &Transform) -> GeometryResult<Transform2> {
let m = full.to_geom().matrix3;
let translation = full.to_geom().translation;
let z_leak = m.z_axis.x.abs() + m.z_axis.y.abs() + m.x_axis.z.abs() + m.y_axis.z.abs();
if z_leak > 1e-9 || translation.z.abs() > 1e-9 {
return Err(GeometryError::Unsupported {
entity: EntityId(0),
type_name: "IFCCARTESIANTRANSFORMATIONOPERATOR2D".to_string(),
detail: "profile operator has out-of-plane components",
});
}
Ok(Transform2::from_cols(
m.x_axis.truncate(),
m.y_axis.truncate(),
translation.truncate(),
))
}
fn center_line(model: &Model, slots: &Slots<'_>, units: &UnitScale) -> GeometryResult<Profile> {
let path = open::open_polyline_path(
model,
slots.req_ref(section_slot::CL_CURVE, "Curve")?,
units,
)?;
let thickness = units.length(slots.req_f64(section_slot::CL_THICKNESS, "Thickness")?);
Ok(Profile::CenterLine(CenterLineProfile::from_width(
path, thickness,
)))
}