use axiolid_model::{CurveStation, GeometryNode, NodeId, OrientedCurveStation, Station};
use ifc_model::EntityId;
use crate::error::GeometryResult;
use crate::lower::curve::gradient::keeps_vertical;
use crate::lower::curve::lower_curve_node;
use crate::lower::session::LoweringSession;
use crate::transform::Transform;
pub(crate) mod axes;
pub(crate) mod offset;
pub(crate) mod read;
pub(crate) mod seams;
#[cfg(test)]
mod tests;
pub(crate) const POINT: &str = "IFCPOINTBYDISTANCEEXPRESSION";
pub(crate) const PLACEMENT: &str = "IFCAXIS2PLACEMENTLINEAR";
const KIND: &str = "station";
const FRAME: &str = "a station is measured along its basis curve and offset left and up in \
its frame; only a frame that is rigid and keeps world Z keeps that \
measure, its left and the global XY plane the vertical offset is read \
against";
#[derive(Debug, Clone)]
pub(crate) struct Basis {
pub node: NodeId,
seams: Result<Vec<f64>, &'static str>,
pub length: Option<f64>,
precision: f64,
}
impl Basis {
pub(crate) fn lower(
session: &mut LoweringSession<'_>,
owner: EntityId,
owner_type: &str,
curve: EntityId,
frame: Transform,
) -> GeometryResult<Self> {
if !keeps_vertical(&frame) {
return Err(session.unsupported(owner, owner_type, FRAME));
}
let precision = seams::precision(session, owner, owner_type)?;
let node = lower_curve_node(session, curve, frame)?;
let (seams, length) = match session.atomic_curve(node) {
Some(curve) => (seams::tangent_seams(curve), seams::stated_length(curve)),
None => (Err(seams::RELATION), None),
};
if let Err(reason) = seams {
if reason == seams::BANKED {
return Err(session.unsupported(owner, owner_type, seams::BANKED));
}
}
Ok(Self {
node,
seams,
length,
precision,
})
}
pub(crate) fn tolerance(&self, distance: f64) -> f64 {
self.precision.max(1e-9 * distance.abs().max(1.0))
}
pub(crate) fn check_on_curve(
&self,
session: &LoweringSession<'_>,
owner: EntityId,
owner_type: &str,
distance: f64,
) -> GeometryResult<()> {
if !distance.is_finite() || distance < 0.0 {
return Err(session.degenerate(
owner,
owner_type,
format!("DistanceAlong {distance} is before the basis curve's start"),
));
}
if let Some(length) = self.length {
if distance > length + self.tolerance(length) {
return Err(session.degenerate(
owner,
owner_type,
format!("DistanceAlong {distance} is beyond the basis curve's length {length}"),
));
}
}
Ok(())
}
pub(crate) fn check_no_seam(
&self,
session: &LoweringSession<'_>,
owner: EntityId,
owner_type: &str,
from: f64,
to: f64,
reason: &'static str,
) -> GeometryResult<()> {
let seams = self
.seams
.as_ref()
.map_err(|why| session.unsupported(owner, owner_type, why))?;
let hit = seams
.iter()
.any(|seam| *seam >= from - self.tolerance(from) && *seam <= to + self.tolerance(to));
if hit {
Err(session.unsupported(owner, owner_type, reason))
} else {
Ok(())
}
}
}
pub fn lower_point_by_distance_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
memoized(session, id, frame, |session| {
let (basis, station) = located(session, id, POINT, id, frame)?;
session.node_for(
id,
GeometryNode::CurveStation(CurveStation::new(basis.node, station)),
)
})
}
pub fn lower_axis2_placement_linear_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
memoized(session, id, frame, |session| {
let placement = axes::read_placement(session, id)?;
let (basis, station) = located(session, id, PLACEMENT, placement.location, frame)?;
let orientation = axes::placement_orientation(session, id, &placement)?;
session.node_for(
id,
GeometryNode::OrientedCurveStation(OrientedCurveStation::new(
CurveStation::new(basis.node, station),
orientation,
)),
)
})
}
const ON_SEAM: &str = "DistanceAlong falls on a tangent discontinuity of the basis curve, where \
IFC4.3 ADD2 (8.9.3.48.3) lets the previous segment's tangent govern; \
the neutral station reads the next segment's and states no rule";
fn located(
session: &mut LoweringSession<'_>,
owner: EntityId,
owner_type: &str,
point: EntityId,
frame: Transform,
) -> GeometryResult<(Basis, Station)> {
let expression = read::distance_expression(session, owner, owner_type, point)?;
let basis = Basis::lower(session, owner, owner_type, expression.basis, frame)?;
basis.check_on_curve(session, owner, owner_type, expression.distance)?;
basis.check_no_seam(
session,
owner,
owner_type,
expression.distance,
expression.distance,
ON_SEAM,
)?;
Ok((basis, expression.station()))
}
fn memoized(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
build: impl FnOnce(&mut LoweringSession<'_>) -> GeometryResult<NodeId>,
) -> GeometryResult<NodeId> {
if let Some(node) = session.memoized(id, KIND, frame) {
return Ok(node);
}
session.enter(id, KIND)?;
let result = build(session);
session.exit(id);
let node = result?;
session.memoize(id, KIND, frame, node);
Ok(node)
}