use axiolid_core::{Interval, Transform2, Vec2};
use axiolid_curve::{Curve2, Line2};
use axiolid_model::{GeometryNode, NodeId};
use axiolid_profile::{CenterLineProfile, Contour, ContourProfile, Profile, ProfileSegment};
use ifc_model::{EntityId, Model};
use crate::error::{GeometryError, GeometryResult};
use crate::input::profile::{
describe_profile, ProfileDescription, ProfileOperator, ProfileParameters, ProfilePosition,
};
use crate::lower::session::LoweringSession;
use crate::slots::Slots;
use crate::units::UnitScale;
mod composite;
mod open;
mod sections;
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;
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> {
let description = describe_profile(model, units, id)?;
build(model, units, &description)
}
fn build(
model: &Model,
units: &UnitScale,
description: &ProfileDescription,
) -> GeometryResult<Profile> {
let profile = match &description.parameters {
ProfileParameters::ArbitraryClosed { outer_curve } => Profile::Contour(ContourProfile {
outer: curve_to_contour(model, *outer_curve, units)?,
holes: Vec::new(),
}),
ProfileParameters::ArbitraryWithVoids {
outer_curve,
inner_curves,
} => {
let outer = curve_to_contour(model, *outer_curve, units)?;
let mut holes = Vec::with_capacity(inner_curves.len());
for curve in inner_curves {
holes.push(curve_to_contour(model, *curve, units)?);
}
Profile::Contour(ContourProfile { outer, holes })
}
ProfileParameters::ArbitraryOpen { .. } => {
return Err(GeometryError::Unsupported {
entity: description.entity,
type_name: description.type_name.clone(),
detail: "open profiles have no area; use lower_open_profile_node",
});
}
ProfileParameters::CenterLine { curve, thickness } => {
let path = open::open_polyline_path(model, *curve, units)?;
Profile::CenterLine(CenterLineProfile::from_width(path, *thickness))
}
ProfileParameters::Composite { profiles, .. } => {
let mut members = Vec::with_capacity(profiles.len());
for member in profiles {
members.push(build(model, units, member)?);
}
Profile::Composite(members)
}
ProfileParameters::Derived {
parent, operator, ..
} => Profile::Derived {
basis: Box::new(build(model, units, parent)?),
transform: operator_2d(operator),
},
ProfileParameters::Mirrored { parent, .. } => Profile::Derived {
basis: Box::new(build(model, units, parent)?),
transform: Transform2::from_scale(Vec2::new(-1.0, 1.0)),
},
parameterized => sections::parameterized(description, parameterized)?,
};
Ok(match &description.position {
Some(position) => Profile::Derived {
basis: Box::new(profile),
transform: position_2d(position),
},
None => profile,
})
}
fn position_2d(position: &ProfilePosition) -> Transform2 {
Transform2::from_cols(
Vec2::from_array(position.x_axis),
Vec2::from_array(position.y_axis()),
Vec2::from_array(position.origin),
)
}
fn operator_2d(operator: &ProfileOperator) -> Transform2 {
Transform2::from_cols(
Vec2::from_array(operator.x_axis),
Vec2::from_array(operator.y_axis),
Vec2::from_array(operator.origin),
)
}
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();
}
}
#[cfg(test)]
mod tests {
#[test]
fn the_generic_profile_refusal_matches_the_declared_reason() {
assert_eq!(
super::PLANNED_PROFILES[0].1,
crate::input::profile::GENERIC_PROFILE
);
}
}