use axiolid_model::{CurveRelation, GeometryNode, NodeId, Station, StationFrame, StationOffsets};
use ifc_model::EntityId;
use super::read::distance_expression;
use super::Basis;
use crate::error::GeometryResult;
use crate::lower::session::LoweringSession;
use crate::transform::Transform;
pub(crate) const TYPE: &str = "IFCOFFSETCURVEBYDISTANCES";
mod slot {
pub const BASIS_CURVE: usize = 0;
pub const OFFSET_VALUES: usize = 1;
}
const LONGITUDINAL: &str =
"an OffsetValues member states a non-zero OffsetLongitudinal, which IFC4.3 ADD2 defines \
for reaching a point past a tangent discontinuity (8.9.3.48.3), not as a law along a \
curve";
const OTHER_BASIS: &str =
"an OffsetValues member is measured along another BasisCurve than the offset curve's own";
const OVER_SEAM: &str =
"the offsets run over a tangent discontinuity of the basis curve, where an offset curve \
has a gap or a loop and IFC4.3 ADD2 states no join";
const NO_LENGTH: &str =
"the offsets stop short of the basis curve's ends, where IFC4.3 ADD2 (8.9.3.42.3) \
continues them unchanged, and the basis states no length to continue them to; the \
neutral offset curve stops at its last station";
pub(crate) fn offset_curve_by_distances(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let slots = session.slots(id)?;
let basis_curve = slots.req_ref(slot::BASIS_CURVE, "BasisCurve")?;
let members = slots.req_ref_list(slot::OFFSET_VALUES, "OffsetValues")?;
if members.is_empty() {
return Err(session.degenerate(id, TYPE, "OffsetValues is LIST [1:?] and is empty"));
}
let mut expressions = Vec::with_capacity(members.len());
for member in members {
let expression = distance_expression(session, id, TYPE, member)?;
if expression.basis != basis_curve {
return Err(session.unsupported(id, TYPE, OTHER_BASIS));
}
if expression.longitudinal.is_some_and(|g| g != 0.0) {
return Err(session.unsupported(id, TYPE, LONGITUDINAL));
}
expressions.push(expression);
}
let basis = Basis::lower(session, id, TYPE, basis_curve, frame)?;
let mut stations: Vec<Station> = Vec::with_capacity(expressions.len() + 2);
for expression in &expressions {
basis.check_on_curve(session, id, TYPE, expression.distance)?;
if let Some(previous) = stations.last() {
if expression.distance <= previous.distance {
return Err(session.degenerate(
id,
TYPE,
"OffsetValues are sequential: their DistanceAlong must increase strictly",
));
}
}
let mut station = expression.station();
station.offsets.longitudinal = 0.0;
stations.push(station);
}
let first = stations[0];
let last = stations[stations.len() - 1];
let head = first.distance > basis.tolerance(first.distance);
let tail = match basis.length {
Some(length) => last.distance < length - basis.tolerance(length),
None => true,
};
if head || tail {
let Some(length) = basis.length else {
return Err(session.unsupported(id, TYPE, NO_LENGTH));
};
if head {
stations.insert(0, Station::new(0.0, carried(first.offsets)));
}
if tail {
stations.push(Station::new(length, carried(last.offsets)));
}
}
let from = stations[0].distance;
let to = stations[stations.len() - 1].distance;
if stations.len() < 2 || to <= from {
return Err(session.degenerate(
id,
TYPE,
"the offsets and the basis curve leave no extent for the offset curve",
));
}
basis.check_no_seam(session, id, TYPE, from, to, OVER_SEAM)?;
session.node_for(
id,
GeometryNode::CurveRelation(CurveRelation::OffsetByStations {
basis: basis.node,
stations,
frame: StationFrame::Section,
}),
)
}
fn carried(offsets: StationOffsets) -> StationOffsets {
StationOffsets::new(offsets.lateral, offsets.vertical, 0.0)
}