use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Point3, Scalar, Transform3};
use axiolid_curve::{Curve2, Curve3, SeamSide};
use axiolid_curve_evaluate_contract::DistanceConvention;
use super::seam::tidy;
use super::{
admitted, invalid, start2, start3, station_length2, station_length3, station_seams2,
station_seams3, station_section2_on, station_section3_on, SectionFrame, StationSeam,
};
use crate::arc_parameter::{
arc_length2, arc_length3, parameter_at_arc_length2, parameter_at_arc_length3,
ARC_LENGTH_TOLERANCE,
};
use crate::curve::{evaluate2, evaluate3};
pub const JOINT_TOLERANCE: Scalar = 1e-9;
fn slack(distance: Scalar) -> Scalar {
ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
}
fn unsupported(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
input,
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StationCurve<'c> {
Two(&'c Curve2),
Three(&'c Curve3),
}
impl StationCurve<'_> {
#[must_use]
pub fn convention(self) -> DistanceConvention {
match self {
Self::Three(Curve3::Elevated(_) | Curve3::Banked(_)) => {
DistanceConvention::PlanDistance
}
_ => DistanceConvention::ArcLength3d,
}
}
fn length(self) -> GeomResult<Option<Scalar>> {
match self {
Self::Two(curve) => station_length2(curve),
Self::Three(curve) => station_length3(curve),
}
}
fn is_line(self) -> bool {
matches!(
self,
Self::Two(Curve2::Line(_)) | Self::Three(Curve3::Line(_))
)
}
fn is_closed_conic(self) -> bool {
matches!(
self,
Self::Two(Curve2::Circle(_) | Curve2::Ellipse(_))
| Self::Three(Curve3::Circle(_) | Curve3::Ellipse(_))
)
}
fn measure_is_exact(self) -> bool {
!matches!(
self,
Self::Two(Curve2::Ellipse(_) | Curve2::BSpline(_))
| Self::Three(Curve3::Ellipse(_) | Curve3::BSpline(_))
)
}
fn line_speed(self) -> Option<Scalar> {
match self {
Self::Two(Curve2::Line(line)) => Some(line.direction.length()),
Self::Three(Curve3::Line(line)) => Some(line.direction.length()),
_ => None,
}
}
fn folded(self, m: Scalar) -> GeomResult<Scalar> {
if !self.is_closed_conic() {
return Ok(m);
}
let turn = self.length()?.unwrap_or(Scalar::INFINITY);
Ok(if m > turn { m - turn } else { m })
}
fn measure_at(self, t: Scalar) -> GeomResult<Scalar> {
Ok(match self {
Self::Two(Curve2::Line(line)) => t * line.direction.length(),
Self::Three(Curve3::Line(line)) => t * line.direction.length(),
Self::Two(Curve2::Circle(circle)) => t * circle.radius,
Self::Three(Curve3::Circle(circle)) => t * circle.radius,
Self::Two(curve @ (Curve2::Polyline(_) | Curve2::BSpline(_) | Curve2::Ellipse(_))) => {
arc_length2(curve, start2(curve), t)?
}
Self::Three(
curve @ (Curve3::Polyline(_) | Curve3::BSpline(_) | Curve3::Ellipse(_)),
) => arc_length3(curve, start3(curve), t)?,
_ => t,
})
}
fn point_at(self, m: Scalar) -> GeomResult<Point3> {
let m = self.folded(m)?;
match self {
Self::Two(curve) => {
let t = match curve {
Curve2::Line(line) => m / line.direction.length(),
_ => parameter_at_arc_length2(curve, start2(curve), m)?,
};
let p = evaluate2(curve, t)?;
Ok(Point3::new(p.x, p.y, 0.0))
}
Self::Three(curve) => match curve {
Curve3::Elevated(elevated) => crate::arc_length::elevated_point(elevated, m),
Curve3::Banked(banked) => crate::banked::banked_point(banked, m),
Curve3::Line(line) => evaluate3(curve, m / line.direction.length()),
_ => evaluate3(curve, parameter_at_arc_length3(curve, start3(curve), m)?),
},
}
}
fn section_at(self, m: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
let m = self.folded(m)?;
match self {
Self::Two(Curve2::Line(line)) => {
let point = line.origin + line.direction * (m / line.direction.length());
super::planar_frame(point, line.direction)
}
Self::Three(curve @ Curve3::Line(line)) => super::reference_up_frame(
evaluate3(curve, m / line.direction.length())?,
line.direction,
),
Self::Two(curve) => station_section2_on(curve, m, side),
Self::Three(curve) => station_section3_on(curve, m, side),
}
}
fn seams(self) -> GeomResult<Vec<StationSeam>> {
if self.is_closed_conic() {
return Ok(Vec::new());
}
match self {
Self::Two(curve) => station_seams2(curve),
Self::Three(curve) => station_seams3(curve),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StationPiece<'c> {
pub curve: StationCurve<'c>,
pub start: Scalar,
pub end: Scalar,
pub reversed: bool,
pub placement: Option<Transform3>,
pub placement_exact: bool,
}
impl<'c> StationPiece<'c> {
pub fn whole(curve: StationCurve<'c>) -> GeomResult<Self> {
let end = match curve.line_speed() {
Some(speed) => speed,
None => curve.length()?.ok_or_else(|| {
invalid("station: a composite piece has no bounded length".into())
})?,
};
Self::between(curve, 0.0, end)
}
pub fn between(curve: StationCurve<'c>, start: Scalar, end: Scalar) -> GeomResult<Self> {
if !(start.is_finite() && end.is_finite()) || end <= start {
return Err(GeomError::Degenerate(format!(
"station: a composite piece spans [{start}, {end}], which is empty or not finite"
)));
}
if curve
.line_speed()
.is_some_and(|speed| !(speed.is_finite() && speed > 0.0))
{
return Err(GeomError::Degenerate(
"station: a composite piece is a line with no direction".into(),
));
}
if !curve.is_line() {
let length = curve.length()?.unwrap_or(Scalar::INFINITY);
let reach = if curve.is_closed_conic() {
start + length
} else {
length
};
if start < -slack(0.0) || start > length + slack(length) || end > reach + slack(reach) {
return Err(invalid(format!(
"station: a composite piece spans [{start}, {end}], outside its curve's \
measure [0, {length}]"
)));
}
}
Ok(Self {
curve,
start,
end,
reversed: false,
placement: None,
placement_exact: true,
})
}
pub fn between_parameters(curve: StationCurve<'c>, t0: Scalar, t1: Scalar) -> GeomResult<Self> {
if !(t0.is_finite() && t1.is_finite()) || t1 <= t0 {
return Err(GeomError::Degenerate(format!(
"station: a composite piece spans parameters [{t0}, {t1}], which is empty or \
not finite"
)));
}
if curve.is_closed_conic() {
let turn = core::f64::consts::TAU;
let from = t0.rem_euclid(turn);
let to = from + (t1 - t0);
let start = curve.measure_at(from)?;
let end = if to <= turn {
curve.measure_at(to)?
} else {
let length = curve.length()?.unwrap_or(Scalar::INFINITY);
length + curve.measure_at(to - turn)?
};
return Self::between(curve, start, end);
}
Self::between(curve, curve.measure_at(t0)?, curve.measure_at(t1)?)
}
#[must_use]
pub const fn reversed(mut self) -> Self {
self.reversed = !self.reversed;
self
}
#[must_use]
pub fn placed(mut self, rigid: Transform3, exact: bool) -> Self {
self.placement = Some(match self.placement {
Some(inner) => rigid * inner,
None => rigid,
});
self.placement_exact &= exact;
self
}
#[must_use]
pub fn length(&self) -> Scalar {
self.end - self.start
}
#[must_use]
pub fn convention(&self) -> DistanceConvention {
self.curve.convention()
}
#[must_use]
pub fn frame_is_exact(&self) -> bool {
self.curve.is_line() && self.placement_exact
}
fn measure(&self, u: Scalar, side: SeamSide) -> (Scalar, SeamSide) {
if self.reversed {
let swapped = match side {
SeamSide::Incoming => SeamSide::Outgoing,
_ => SeamSide::Incoming,
};
(self.end - u, swapped)
} else {
(self.start + u, side)
}
}
fn point_at(&self, u: Scalar) -> GeomResult<Point3> {
let (m, _) = self.measure(u, SeamSide::Outgoing);
let point = self.curve.point_at(m)?;
Ok(match self.placement {
Some(rigid) => rigid.transform_point3(point),
None => point,
})
}
pub fn section_on(&self, u: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
let length = self.length();
let u = u.clamp(0.0, length);
let side = if u <= slack(u) {
SeamSide::Outgoing
} else if u >= length - slack(length) {
SeamSide::Incoming
} else {
side
};
let (m, curve_side) = self.measure(u, side);
let mut section = self.curve.section_at(m, curve_side)?;
if self.reversed {
section.tangent = -section.tangent;
section.lateral = -section.lateral;
}
match self.placement {
Some(rigid) => section.carried(rigid, self.convention()),
None => Ok(section),
}
}
fn seams(&self) -> GeomResult<Vec<StationSeam>> {
let mut out: Vec<StationSeam> = self
.curve
.seams()?
.into_iter()
.filter(|seam| {
seam.distance > self.start + slack(self.start)
&& seam.distance < self.end - slack(self.end)
})
.map(|seam| {
let u = if self.reversed {
self.end - seam.distance
} else {
seam.distance - self.start
};
StationSeam::new(u, u, seam.smooth, seam.exact)
})
.collect();
out.sort_by(|a, b| a.distance.total_cmp(&b.distance));
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompositeBasis<'c> {
pieces: Vec<StationPiece<'c>>,
starts: Vec<Scalar>,
convention: DistanceConvention,
}
impl<'c> CompositeBasis<'c> {
pub fn new(pieces: Vec<StationPiece<'c>>) -> GeomResult<Self> {
let Some(first) = pieces.first() else {
return Err(invalid("station: a composite basis has no pieces".into()));
};
let convention = first.convention();
for (index, piece) in pieces.iter().enumerate() {
let length = piece.length();
if !length.is_finite() || length <= 0.0 {
return Err(GeomError::Degenerate(format!(
"station: piece {index} of a composite basis has no length"
)));
}
if piece.convention() != convention {
return Err(unsupported(
"a composite station basis whose pieces measure stations differently (plan \
distance on an elevated or banked piece, arc length on another): no one \
distance runs through it",
));
}
}
for index in 1..pieces.len() {
check_joint(&pieces[index - 1], &pieces[index], index)?;
}
let mut starts = Vec::with_capacity(pieces.len() + 1);
let mut run = 0.0;
for piece in &pieces {
starts.push(run);
run += piece.length();
}
starts.push(run);
Ok(Self {
pieces,
starts,
convention,
})
}
#[must_use]
pub fn pieces(&self) -> &[StationPiece<'c>] {
&self.pieces
}
#[must_use]
pub fn length(&self) -> Scalar {
self.starts[self.pieces.len()]
}
#[must_use]
pub fn convention(&self) -> DistanceConvention {
self.convention
}
fn locate(&self, distance: Scalar, side: SeamSide) -> (usize, Scalar) {
let count = self.pieces.len();
for joint in 1..count {
let at = self.starts[joint];
if (at - distance).abs() <= slack(distance) {
return match side {
SeamSide::Incoming => (joint - 1, self.pieces[joint - 1].length()),
_ => (joint, 0.0),
};
}
}
let k = self.starts[1..count].partition_point(|start| *start <= distance);
(k, distance - self.starts[k])
}
pub fn section_on(&self, distance: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
let distance = admitted(distance, Some(self.length()))?;
let (piece, into) = self.locate(distance, side);
self.pieces[piece].section_on(into, side)
}
#[must_use]
pub fn frame_is_exact_at(&self, distance: Scalar, side: SeamSide) -> bool {
let Ok(distance) = admitted(distance, Some(self.length())) else {
return false;
};
let (piece, _) = self.locate(distance, side);
self.pieces[..=piece]
.iter()
.all(StationPiece::frame_is_exact)
}
pub fn seams(&self) -> GeomResult<Vec<StationSeam>> {
let mut out = Vec::new();
let mut exact_before = true;
for (k, piece) in self.pieces.iter().enumerate() {
let at = self.starts[k];
if k > 0 {
out.push(StationSeam::new(at, at, false, exact_before));
}
for seam in piece.seams()? {
let distance = at + seam.distance;
out.push(StationSeam::new(
distance,
distance,
seam.smooth,
seam.exact && exact_before,
));
}
exact_before &= piece.curve.measure_is_exact();
}
Ok(tidy(out, Some(self.length())))
}
pub fn exact_seams(&self) -> GeomResult<Vec<StationSeam>> {
let seams = self.seams()?;
if seams.iter().any(|seam| !seam.exact) {
return Err(unsupported(
"exact seam distances of a composite with an ellipse or a B-spline piece before \
a seam, or a B-spline's corner knot: their arc length is a quadrature",
));
}
Ok(seams)
}
pub fn pieces_between(&self, start: Scalar, end: Scalar) -> GeomResult<Vec<StationPiece<'c>>> {
let length = self.length();
if !(start.is_finite() && end.is_finite()) || end <= start {
return Err(GeomError::Degenerate(format!(
"station: a trim of a composite basis spans [{start}, {end}], which is empty or \
not finite"
)));
}
if start < -slack(0.0) || end > length + slack(length) {
return Err(invalid(format!(
"station: a trim of a composite basis spans [{start}, {end}], outside its \
length {length}"
)));
}
let (start, end) = (start.max(0.0), end.min(length));
let mut out = Vec::new();
for (k, piece) in self.pieces.iter().enumerate() {
let (lo, hi) = (self.starts[k], self.starts[k + 1]);
let (a, b) = (start.max(lo) - lo, end.min(hi) - lo);
if b - a <= slack(b) {
continue;
}
let mut clipped = *piece;
if piece.reversed {
clipped.start = piece.end - b;
clipped.end = piece.end - a;
} else {
clipped.start = piece.start + a;
clipped.end = piece.start + b;
}
out.push(clipped);
}
Ok(out)
}
}
fn check_joint(
before: &StationPiece<'_>,
after: &StationPiece<'_>,
index: usize,
) -> GeomResult<()> {
let end = before.point_at(before.length())?;
let start = after.point_at(0.0)?;
let tolerance = |p: Point3| JOINT_TOLERANCE * p.abs().max_element().max(1.0);
let meets = |a: Point3, b: Point3| (a - b).length() <= tolerance(a);
if meets(end, start) {
return Ok(());
}
let after_end = after.point_at(after.length())?;
let reversed = meets(end, after_end)
|| (index == 1 && {
let before_start = before.point_at(0.0)?;
meets(before_start, start) || meets(before_start, after_end)
});
if reversed {
return Err(invalid(format!(
"station: a reversed piece at joint {index} of a composite basis: its pieces meet \
end to end or start to start, so one runs against the composite (its sense is not \
declared)"
)));
}
Err(invalid(format!(
"station: a gap of {} at joint {index} of a composite basis: its pieces do not meet, so \
no distance runs across it",
(end - start).length()
)))
}