use super::{CompositeCurve, CompositeCurveSegment, KernelPreference, TrimmedCurve};
use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{BSplineCurve2, Circle2, Curve2, Ellipse2, Line2, Polyline2};
use axiolid_model::{
CurveRelation, CurveSegment, GeometryNode, NodeId, Transition, TrimSelector, TrimmingPreference,
};
use ifc_model::EntityId;
use crate::curve::bspline::BSplineCurve;
use crate::curve::conic::{Circle, Ellipse};
use crate::curve::line::Line;
use crate::curve::polyline::{IndexedPolyCurve, PolySegment, Polyline};
use crate::error::GeometryResult;
use crate::lower::curve::{bspline_knot_spec, finite_values};
use crate::lower::session::LoweringSession;
use crate::resource::direction::resolve_unit;
use crate::resource::placement::Axis2Placement2D;
use crate::resource::point::{CartesianPoint, CartesianPointList2D};
pub(super) fn parameter_reference_curve(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let type_name = session.type_name(id)?;
match type_name.as_str() {
"IFCPOLYLINE" => parameter_space_polyline(session, owner, id),
"IFCINDEXEDPOLYCURVE" => parameter_space_indexed_polycurve(session, owner, id),
"IFCLINE" => parameter_space_line(session, owner, id),
"IFCCIRCLE" => parameter_space_circle(session, owner, id),
"IFCELLIPSE" => parameter_space_ellipse(session, owner, id),
"IFCBSPLINECURVEWITHKNOTS" | "IFCRATIONALBSPLINECURVEWITHKNOTS" => {
parameter_space_bspline(session, owner, id)
}
"IFCTRIMMEDCURVE" => parameter_space_trimmed(session, owner, id),
"IFCCOMPOSITECURVE" | "IFCCOMPOSITECURVEONSURFACE" => {
parameter_space_composite(session, owner, id)
}
_ => Err(session.unsupported(
id,
&type_name,
"parameter-space curve family (only exact IfcPolyline, line-only \
IfcIndexedPolyCurve, IfcLine, IfcCircle, IfcEllipse, \
IfcTrimmedCurve, IfcCompositeCurve and explicit-knot B-splines \
are supported)",
)),
}
}
fn parameter_space_polyline(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = Polyline::new(id, entity);
let point_refs = view.point_refs()?;
let closed = point_refs.first() == point_refs.last();
let points = parameter_space_points(session, owner, &point_refs)?;
session.node_for(
id,
GeometryNode::Curve2(Curve2::Polyline(Polyline2 { points, closed })),
)
}
fn parameter_space_indexed_polycurve(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = IndexedPolyCurve::new(id, entity);
let explicit = view.has_explicit_segments();
let point_list_ref = view.points_ref()?;
let list_entity = session.entity(owner, point_list_ref)?;
if !list_entity
.type_name
.eq_ignore_ascii_case("IFCCARTESIANPOINTLIST2D")
{
return Err(session.unsupported(
point_list_ref,
&list_entity.type_name,
"parameter-space indexed polycurve needs a 2D point list",
));
}
let coordinates = CartesianPointList2D::new(point_list_ref, list_entity).coordinates()?;
let mut points = Vec::with_capacity(coordinates.len());
for xy in coordinates {
if !xy.iter().all(|value| value.is_finite()) {
return Err(session.degenerate(
point_list_ref,
"IFCCARTESIANPOINTLIST2D",
"parameter-space point must contain two finite coordinates",
));
}
points.push(Point2::from_array(xy));
}
let segments = view.segments(points.len())?;
let closed = points.first() == points.last() && points.len() > 1;
if !explicit {
return session.node_for(
id,
GeometryNode::Curve2(Curve2::Polyline(Polyline2 { points, closed })),
);
}
let mut children = Vec::with_capacity(segments.len());
for segment in segments {
let curve = match segment {
PolySegment::Line(indices) => {
let run_closed = indices.first() == indices.last();
let mut selected: Vec<_> = indices.into_iter().map(|i| points[i]).collect();
if run_closed && selected.len() > 1 {
selected.pop();
}
session.node_for(
id,
GeometryNode::Curve2(Curve2::Polyline(Polyline2 {
points: selected,
closed: run_closed,
})),
)?
}
PolySegment::Arc { start, mid, end } => {
parameter_space_arc(session, id, points[start], points[mid], points[end])?
}
};
children.push(CurveSegment {
curve,
same_sense: true,
transition: Transition::Continuous,
});
}
session.node_for(
id,
GeometryNode::CurveRelation(CurveRelation::Composite { segments: children }),
)
}
fn parameter_space_point(
session: &mut LoweringSession<'_>,
owner: EntityId,
point_ref: EntityId,
) -> GeometryResult<Point2> {
let entity = session.entity(owner, point_ref)?;
let coordinates = CartesianPoint::new(point_ref, entity).coordinates()?;
if coordinates.len() != 2 || !coordinates.iter().all(|value| value.is_finite()) {
return Err(session.degenerate(
point_ref,
"IFCCARTESIANPOINT",
"parameter-space point must contain two finite coordinates",
));
}
Ok(Point2::from_array([coordinates[0], coordinates[1]]))
}
fn parameter_space_frame(
session: &mut LoweringSession<'_>,
owner: EntityId,
position_ref: EntityId,
) -> GeometryResult<Frame2> {
let position = session.entity(owner, position_ref)?;
if !position
.type_name
.eq_ignore_ascii_case("IFCAXIS2PLACEMENT2D")
{
return Err(session.unsupported(
position_ref,
&position.type_name,
"parameter-space conic needs an IfcAxis2Placement2D; a 3D placement \
has no meaning in a surface parameter domain",
));
}
let view = Axis2Placement2D::new(position_ref, position);
let origin = view.location(session.model())?;
let x = view.ref_direction(session.model())?;
if !origin.iter().chain(x.iter()).all(|value| value.is_finite()) {
return Err(session.degenerate(
position_ref,
"IFCAXIS2PLACEMENT2D",
"parameter-space placement must be finite",
));
}
if x[0].hypot(x[1]) == 0.0 {
return Err(session.degenerate(
position_ref,
"IFCAXIS2PLACEMENT2D",
"parameter-space placement RefDirection is zero-length",
));
}
Ok(Frame2 {
origin: Point2::from_array([origin[0], origin[1]]),
x: Vec2::new(x[0], x[1]),
y: Vec2::new(-x[1], x[0]),
})
}
fn parameter_space_line(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = Line::new(id, entity);
let point_ref = view.point_ref()?;
let vector_ref = view.direction_vector_ref()?;
let origin = parameter_space_point(session, owner, point_ref)?;
let vector_entity = session.entity(owner, vector_ref)?;
let slots = crate::slots::Slots::new(vector_ref, vector_entity);
let direction_ref = slots.req_ref(0, "Orientation")?;
let magnitude = slots.req_f64(1, "Magnitude")?;
let unit = resolve_unit(session.model(), owner, direction_ref)?;
let direction = Vec2::new(unit[0] * magnitude, unit[1] * magnitude);
if !magnitude.is_finite() || !direction.x.is_finite() || !direction.y.is_finite() {
return Err(session.degenerate(
vector_ref,
"IFCVECTOR",
"parameter-space line direction must be finite",
));
}
if direction.x == 0.0 && direction.y == 0.0 {
return Err(session.degenerate(
vector_ref,
"IFCVECTOR",
"parameter-space line direction is zero-length",
));
}
session.node_for(
id,
GeometryNode::Curve2(Curve2::Line(Line2 { origin, direction })),
)
}
fn parameter_space_circle(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = Circle::new(id, entity);
let radius = view.radius()?;
let position_ref = view.position_ref()?;
if !radius.is_finite() {
return Err(session.degenerate(
id,
"IFCCIRCLE",
"parameter-space circle radius must be finite",
));
}
let frame = parameter_space_frame(session, owner, position_ref)?;
session.node_for(
id,
GeometryNode::Curve2(Curve2::Circle(Circle2 { frame, radius })),
)
}
fn parameter_space_ellipse(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = Ellipse::new(id, entity);
let (semi_axis_x, semi_axis_y) = view.semi_axes()?;
if !semi_axis_x.is_finite() || !semi_axis_y.is_finite() {
return Err(session.degenerate(
id,
"IFCELLIPSE",
"parameter-space ellipse semi-axes must be finite",
));
}
let position_ref = view.position_ref()?;
let frame = parameter_space_frame(session, owner, position_ref)?;
session.node_for(
id,
GeometryNode::Curve2(Curve2::Ellipse(Ellipse2 {
frame,
semi_axis_x,
semi_axis_y,
})),
)
}
fn parameter_space_points(
session: &mut LoweringSession<'_>,
owner: EntityId,
point_refs: &[EntityId],
) -> GeometryResult<Vec<Point2>> {
let mut points = Vec::with_capacity(point_refs.len());
for &point_ref in point_refs {
points.push(parameter_space_point(session, owner, point_ref)?);
}
Ok(points)
}
fn parameter_space_bspline(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let type_name = session.type_name(id)?;
let entity = session.entity(owner, id)?;
let view = BSplineCurve::new(id, entity);
let degree = u16::try_from(view.degree()?)
.map_err(|_| session.degenerate(id, &type_name, "Degree exceeds u16"))?;
let knots = view.knots()?.ok_or_else(|| {
session.unsupported(id, &type_name, "explicit knots required in parameter space")
})?;
finite_values(session, id, &type_name, "Knots", &knots.values)?;
let declared: u128 = knots.multiplicities.iter().map(|m| *m as u128).sum();
session.check_aggregate(id, &type_name, "knot multiplicities", declared)?;
let multiplicities = knots
.multiplicities
.into_iter()
.map(|value| {
u32::try_from(value)
.map_err(|_| session.degenerate(id, &type_name, "multiplicity exceeds u32"))
})
.collect::<GeometryResult<Vec<_>>>()?;
let refs = view.control_point_refs()?;
let control_points = parameter_space_points(session, owner, &refs)?;
let weights = view.weights()?;
if let Some(values) = weights.as_deref() {
finite_values(session, id, &type_name, "WeightsData", values)?;
}
let closed = view.closed_curve().ok_or_else(|| {
session.unsupported(
id,
&type_name,
"unknown ClosedCurve is not lossless in bool",
)
})?;
session.node_for(
id,
GeometryNode::Curve2(Curve2::BSpline(BSplineCurve2 {
degree,
control_points,
knots: knots.values,
multiplicities,
weights,
closed,
self_intersect: view.self_intersect(),
knot_spec: bspline_knot_spec(view.knot_spec()),
})),
)
}
#[cfg(test)]
mod tests;
fn parameter_space_arc(
session: &mut LoweringSession<'_>,
owner: EntityId,
start: Point2,
mid: Point2,
end: Point2,
) -> GeometryResult<NodeId> {
let u = mid - start;
let v = end - start;
let cross = u.x * v.y - u.y * v.x;
if !cross.is_finite() || cross == 0.0 {
return Err(session.degenerate(
owner,
"IFCINDEXEDPOLYCURVE",
"parameter-space arc points are collinear or non-finite",
));
}
let uu = u.length_squared();
let vv = v.length_squared();
let center = start
+ Vec2::new(
(uu * v.y - vv * u.y) / (2.0 * cross),
(vv * u.x - uu * v.x) / (2.0 * cross),
);
let radial = start - center;
let radius = radial.length();
if !radius.is_finite() || radius <= 0.0 {
return Err(session.degenerate(
owner,
"IFCINDEXEDPOLYCURVE",
"parameter-space arc circumcenter arithmetic overflowed",
));
}
let x = radial / radius;
let frame = Frame2 {
origin: center,
x,
y: Vec2::new(-x.y, x.x),
};
let basis = session.node_for(
owner,
GeometryNode::Curve2(Curve2::Circle(Circle2 { frame, radius })),
)?;
session.node_for(
owner,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Point2(start)],
end: vec![TrimSelector::Point2(end)],
sense_agreement: cross > 0.0,
preference: TrimmingPreference::Cartesian,
}),
)
}
fn parameter_space_trimmed(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = TrimmedCurve::new(id, entity);
let basis_ref = view.basis_curve_ref()?;
let basis = parameter_reference_curve(session, owner, basis_ref)?;
let spec = view.spec()?;
let (t1, t2) = spec.endpoints();
let start = parameter_space_selectors(session, owner, t1)?;
let end = parameter_space_selectors(session, owner, t2)?;
if start.is_empty() || end.is_empty() {
return Err(session.degenerate(
id,
"IFCTRIMMEDCURVE",
"a trim end carries neither a parameter nor a point",
));
}
session.node_for(
id,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start,
end,
sense_agreement: view.sense_agreement()?,
preference: match view.master_representation() {
crate::curve::trimmed::TrimmingPreference::Cartesian => KernelPreference::Cartesian,
crate::curve::trimmed::TrimmingPreference::Parameter => KernelPreference::Parameter,
crate::curve::trimmed::TrimmingPreference::Unspecified => {
KernelPreference::Unspecified
}
},
}),
)
}
fn parameter_space_selectors(
session: &mut LoweringSession<'_>,
owner: EntityId,
trim: crate::curve::trimmed::Trim,
) -> GeometryResult<Vec<TrimSelector>> {
let mut out = Vec::new();
if let Some(raw) = trim.parameter {
out.push(TrimSelector::Parameter(raw));
}
if let Some(point_ref) = trim.cartesian {
let point = parameter_space_point(session, owner, point_ref)?;
out.push(TrimSelector::Point2(point));
}
Ok(out)
}
fn parameter_space_composite(
session: &mut LoweringSession<'_>,
owner: EntityId,
id: EntityId,
) -> GeometryResult<NodeId> {
let entity = session.entity(owner, id)?;
let view = CompositeCurve::new(id, entity);
let segment_refs = view.segment_refs()?;
if segment_refs.is_empty() {
return Err(session.degenerate(id, "IFCCOMPOSITECURVE", "no segments"));
}
let mut segments = Vec::with_capacity(segment_refs.len());
for segment_ref in &segment_refs {
let segment_entity = session.entity(id, *segment_ref)?;
let segment = CompositeCurveSegment::new(*segment_ref, segment_entity);
let parent = segment.parent_curve_ref()?;
segments.push(CurveSegment {
curve: parameter_reference_curve(session, id, parent)?,
same_sense: segment.same_sense()?,
transition: super::transition(segment.transition()?),
});
}
session.node_for(
id,
GeometryNode::CurveRelation(CurveRelation::Composite { segments }),
)
}