mod outline;
mod section;
mod types;
pub use outline::{profile_outline, ProfileOutline};
pub use types::{ProfileDescription, ProfileOperator, ProfileParameters, ProfilePosition};
use ifc_model::{EntityId, Model};
use crate::error::{GeometryError, GeometryResult};
use crate::resource::operator::operator_transform;
use crate::slots::{profile_slot as slot, section_slot, Slots};
use crate::units::UnitScale;
pub(crate) const GENERIC_PROFILE: &str =
"generic profile declaration carries no concrete geometry to lower";
const MAX_PROFILE_DEPTH: usize = 16;
const KIND: &str = "profile";
pub fn describe_profile(
model: &Model,
units: &UnitScale,
profile: EntityId,
) -> GeometryResult<ProfileDescription> {
let mut chain = Vec::new();
describe(model, units, profile, profile, &mut chain)
}
fn describe(
model: &Model,
units: &UnitScale,
referrer: EntityId,
id: EntityId,
chain: &mut Vec<EntityId>,
) -> GeometryResult<ProfileDescription> {
if chain.contains(&id) {
return Err(GeometryError::CyclicChain {
entity: id,
kind: KIND,
});
}
if chain.len() >= MAX_PROFILE_DEPTH {
return Err(GeometryError::ChainTooDeep {
entity: id,
kind: KIND,
limit: MAX_PROFILE_DEPTH,
});
}
let entity = model.get(id).ok_or(GeometryError::MissingEntity {
referrer,
missing: id,
})?;
let slots = Slots::new(id, entity);
let type_name = entity.type_name.to_ascii_uppercase();
chain.push(id);
let parameters = parameters(model, units, &slots, &type_name, chain);
chain.pop();
let parameters = parameters?;
let position = if is_parameterized(¶meters) {
position(model, units, &slots)?
} else {
None
};
Ok(ProfileDescription {
entity: id,
type_name,
name: slots.opt_text(slot::PROFILE_NAME),
position,
parameters,
})
}
fn parameters(
model: &Model,
units: &UnitScale,
slots: &Slots<'_>,
type_name: &str,
chain: &mut Vec<EntityId>,
) -> GeometryResult<ProfileParameters> {
let parameters = match type_name {
"IFCRECTANGLEPROFILEDEF" => section::rectangle(slots, units, false)?,
"IFCROUNDEDRECTANGLEPROFILEDEF" => section::rectangle(slots, units, true)?,
"IFCRECTANGLEHOLLOWPROFILEDEF" => section::rectangle_hollow(slots, units)?,
"IFCCIRCLEPROFILEDEF" => section::circle(slots, units, false)?,
"IFCCIRCLEHOLLOWPROFILEDEF" => section::circle(slots, units, true)?,
"IFCELLIPSEPROFILEDEF" => section::ellipse(slots, units)?,
"IFCISHAPEPROFILEDEF" => section::i_shape(slots, units)?,
"IFCASYMMETRICISHAPEPROFILEDEF" => section::asymmetric_i(slots, units, is_ifc2x3(model))?,
"IFCLSHAPEPROFILEDEF" => section::l_shape(slots, units)?,
"IFCTSHAPEPROFILEDEF" => section::t_shape(slots, units)?,
"IFCUSHAPEPROFILEDEF" => section::u_shape(slots, units)?,
"IFCCSHAPEPROFILEDEF" => section::c_shape(slots, units)?,
"IFCZSHAPEPROFILEDEF" => section::z_shape(slots, units)?,
"IFCTRAPEZIUMPROFILEDEF" => section::trapezium(slots, units)?,
"IFCARBITRARYCLOSEDPROFILEDEF" => ProfileParameters::ArbitraryClosed {
outer_curve: curve_ref(model, slots, slot::OUTER_CURVE, "OuterCurve")?,
},
"IFCARBITRARYPROFILEDEFWITHVOIDS" => {
let outer_curve = curve_ref(model, slots, slot::OUTER_CURVE, "OuterCurve")?;
let inner_curves = slots.req_ref_list(slot::INNER_CURVES, "InnerCurves")?;
for curve in &inner_curves {
slots.resolve(model, *curve)?;
}
ProfileParameters::ArbitraryWithVoids {
outer_curve,
inner_curves,
}
}
"IFCARBITRARYOPENPROFILEDEF" => ProfileParameters::ArbitraryOpen {
curve: curve_ref(model, slots, slot::OUTER_CURVE, "Curve")?,
},
"IFCCENTERLINEPROFILEDEF" => ProfileParameters::CenterLine {
curve: curve_ref(model, slots, section_slot::CL_CURVE, "Curve")?,
thickness: units.length(slots.req_f64(section_slot::CL_THICKNESS, "Thickness")?),
},
"IFCCOMPOSITEPROFILEDEF" => {
let refs = slots.req_ref_list(section_slot::COMPOSITE_PROFILES, "Profiles")?;
let mut profiles = Vec::with_capacity(refs.len());
for member in refs {
profiles.push(describe(model, units, slots.id(), member, chain)?);
}
ProfileParameters::Composite {
profiles,
label: slots.opt_text(section_slot::COMPOSITE_LABEL),
}
}
"IFCDERIVEDPROFILEDEF" => {
let parent = slots.req_ref(section_slot::DERIVED_PARENT, "ParentProfile")?;
let parent = Box::new(describe(model, units, slots.id(), parent, chain)?);
ProfileParameters::Derived {
parent,
operator: operator(model, units, slots)?,
label: slots.opt_text(section_slot::DERIVED_LABEL),
}
}
"IFCMIRROREDPROFILEDEF" => {
let parent = slots.req_ref(section_slot::DERIVED_PARENT, "ParentProfile")?;
ProfileParameters::Mirrored {
parent: Box::new(describe(model, units, slots.id(), parent, chain)?),
label: slots.opt_text(section_slot::DERIVED_LABEL),
}
}
"IFCPROFILEDEF" => return Err(slots.unsupported(GENERIC_PROFILE)),
_ => return Err(slots.unsupported("profile family is not interpreted")),
};
Ok(parameters)
}
fn is_parameterized(parameters: &ProfileParameters) -> bool {
!matches!(
parameters,
ProfileParameters::ArbitraryClosed { .. }
| ProfileParameters::ArbitraryWithVoids { .. }
| ProfileParameters::ArbitraryOpen { .. }
| ProfileParameters::CenterLine { .. }
| ProfileParameters::Composite { .. }
| ProfileParameters::Derived { .. }
| ProfileParameters::Mirrored { .. }
)
}
fn is_ifc2x3(model: &Model) -> bool {
model
.header()
.schema_token()
.is_some_and(|token| token.to_ascii_uppercase().starts_with("IFC2X3"))
}
fn curve_ref(
model: &Model,
slots: &Slots<'_>,
index: usize,
name: &'static str,
) -> GeometryResult<EntityId> {
let curve = slots.req_ref(index, name)?;
slots.resolve(model, curve)?;
Ok(curve)
}
fn position(
model: &Model,
units: &UnitScale,
slots: &Slots<'_>,
) -> GeometryResult<Option<ProfilePosition>> {
let Some(position_id) = slots.opt_ref(slot::POSITION) else {
return Ok(None);
};
let position = slots.resolve(model, position_id)?;
let position_slots = Slots::new(position_id, position);
let location_id = position_slots.req_ref(0, "Location")?;
let location = position_slots.resolve(model, 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 = [units.length(coordinates[0]), units.length(coordinates[1])];
let x_axis = match position_slots.opt_ref(1) {
Some(direction_id) => {
let direction = position_slots.resolve(model, 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"));
}
let length = ratios[0].hypot(ratios[1]);
if length == 0.0 || !length.is_finite() {
return Err(position_slots.degenerate("2D reference direction has zero length"));
}
[ratios[0] / length, ratios[1] / length]
}
None => [1.0, 0.0],
};
Ok(Some(ProfilePosition { origin, x_axis }))
}
fn operator(
model: &Model,
units: &UnitScale,
slots: &Slots<'_>,
) -> GeometryResult<ProfileOperator> {
let operator = slots.req_ref(section_slot::DERIVED_OPERATOR, "Operator")?;
let entity = slots.resolve(model, operator)?;
let full = operator_transform(model, operator, entity)?;
let [x, y, z] = full.basis;
let z_leak = z[0].abs() + z[1].abs() + x[2].abs() + y[2].abs();
if z_leak > 1e-9 || full.origin[2].abs() > 1e-9 {
return Err(Slots::new(operator, entity)
.unsupported("profile operator has out-of-plane components"));
}
Ok(ProfileOperator {
x_axis: [x[0], x[1]],
y_axis: [y[0], y[1]],
origin: [units.length(full.origin[0]), units.length(full.origin[1])],
})
}