use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Point2, Point3, Scalar, Vec3};
use axiolid_curve::{
BSplineCurve, Banked3, CantLaw, ChainPiece2, Curve2, Curve3, Elevated3, ElevationLaw, SeamSide,
};
use super::{
banked_frame, invalid, planar_frame, reference_up_frame, station_length2, station_length3,
SectionFrame,
};
use crate::arc_parameter::{arc_length2, arc_length3, ARC_LENGTH_TOLERANCE};
use crate::curve::{derivative2, derivative3, domain2, domain3, evaluate2, evaluate3};
pub const SEAM_TANGENT_TOLERANCE: Scalar = 1e-9;
pub const MITRE_TOLERANCE: Scalar = 1e-6;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StationSeam {
pub distance: Scalar,
pub parameter: Scalar,
pub smooth: bool,
pub exact: bool,
}
impl StationSeam {
#[must_use]
pub const fn new(distance: Scalar, parameter: Scalar, smooth: bool, exact: bool) -> Self {
Self {
distance,
parameter,
smooth,
exact,
}
}
const fn measured(distance: Scalar, smooth: bool) -> Self {
Self::new(distance, distance, smooth, true)
}
}
fn slack(distance: Scalar) -> Scalar {
ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
}
pub(super) fn tidy(mut seams: Vec<StationSeam>, length: Option<Scalar>) -> Vec<StationSeam> {
seams.retain(|seam| {
seam.distance.is_finite()
&& seam.distance > slack(0.0)
&& length.is_none_or(|l| seam.distance < l - slack(l))
});
seams.sort_by(|a, b| a.distance.total_cmp(&b.distance));
let mut out: Vec<StationSeam> = Vec::with_capacity(seams.len());
for seam in seams {
match out.last_mut() {
Some(last) if seam.distance - last.distance <= slack(seam.distance) => {
last.smooth &= seam.smooth;
last.exact &= seam.exact;
}
_ => out.push(seam),
}
}
out
}
fn polyline_segments<P>(
points: &[P],
closed: bool,
length: impl Fn(P, P) -> Scalar,
) -> Vec<(usize, usize, Scalar)>
where
P: Copy,
{
let count = points.len();
let spans: Vec<(usize, usize)> = if closed {
(0..count).map(|i| (i, (i + 1) % count)).collect()
} else {
(0..count.saturating_sub(1)).map(|i| (i, i + 1)).collect()
};
spans
.into_iter()
.map(|(i, j)| (i, j, length(points[i], points[j])))
.collect()
}
fn polyline_seams(segments: &[(usize, usize, Scalar)]) -> Vec<StationSeam> {
let mut out = Vec::new();
let mut run = 0.0;
let mut started = false;
for &(start, _, length) in segments {
if length > 0.0 {
if started {
out.push(StationSeam::new(run, start as Scalar, false, true));
}
started = true;
}
run += length;
}
out
}
fn polyline_side(
segments: &[(usize, usize, Scalar)],
vertex: usize,
side: SeamSide,
) -> GeomResult<(usize, usize)> {
let at = segments
.iter()
.position(|&(start, _, _)| start == vertex)
.ok_or_else(|| invalid("station: the seam is not a vertex of the polyline".into()))?;
let found = match side {
SeamSide::Incoming => segments[..at].iter().rev().find(|s| s.2 > 0.0),
_ => segments[at..].iter().find(|s| s.2 > 0.0),
};
found.map(|&(i, j, _)| (i, j)).ok_or_else(|| {
invalid("station: the polyline has no segment on that side of the seam".into())
})
}
fn corner_knots(breaks: Vec<Scalar>, lo: Scalar, hi: Scalar) -> Vec<Scalar> {
breaks.into_iter().filter(|&t| t > lo && t < hi).collect()
}
fn reversed_spline<P: Clone>(spline: &BSplineCurve<P>) -> (BSplineCurve<P>, Scalar) {
let first = spline.knots.first().copied().unwrap_or(0.0);
let last = spline.knots.last().copied().unwrap_or(0.0);
let pivot = first + last;
let mut reversed = spline.clone();
reversed.knots = spline.knots.iter().rev().map(|t| pivot - t).collect();
reversed.multiplicities.reverse();
reversed.control_points.reverse();
if let Some(weights) = reversed.weights.as_mut() {
weights.reverse();
}
(reversed, pivot)
}
pub fn station_seams2(curve: &Curve2) -> GeomResult<Vec<StationSeam>> {
let length = station_length2(curve)?;
let seams = match curve {
Curve2::Polyline(polyline) => polyline_seams(&polyline_segments(
&polyline.points,
polyline.closed,
|a: Point2, b: Point2| (b - a).length(),
)),
Curve2::BSpline(_) => {
let domain = domain2(curve);
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
corner_knots(crate::bound::continuity_breaks2(curve, 1), lo, hi)
.into_iter()
.map(|t| {
Ok(StationSeam::new(
arc_length2(curve, lo, t)?,
t,
false,
false,
))
})
.collect::<GeomResult<_>>()?
}
Curve2::Intrinsic(intrinsic) => intrinsic
.curvature
.seams_within(intrinsic.length)
.into_iter()
.map(|s| StationSeam::measured(s, true))
.collect(),
Curve2::Chain(chain) => chain
.joins()
.unwrap_or_default()
.into_iter()
.chain(crate::chain::chain_curvature_seams(chain))
.map(|s| StationSeam::measured(s, true))
.collect(),
_ => Vec::new(),
};
Ok(tidy(seams, length))
}
pub fn station_seams3(curve: &Curve3) -> GeomResult<Vec<StationSeam>> {
let length = station_length3(curve)?;
let seams = match curve {
Curve3::Polyline(polyline) => polyline_seams(&polyline_segments(
&polyline.points,
polyline.closed,
|a: Point3, b: Point3| (b - a).length(),
)),
Curve3::BSpline(_) => {
let domain = domain3(curve);
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
corner_knots(crate::bound::continuity_breaks3(curve, 1), lo, hi)
.into_iter()
.map(|t| {
Ok(StationSeam::new(
arc_length3(curve, lo, t)?,
t,
false,
false,
))
})
.collect::<GeomResult<_>>()?
}
Curve3::Intrinsic(intrinsic) => intrinsic
.curvature
.seams_within(intrinsic.length)
.into_iter()
.chain(intrinsic.torsion.seams_within(intrinsic.length))
.map(|s| StationSeam::measured(s, true))
.collect(),
Curve3::Elevated(elevated) => elevated_seams(elevated)?,
Curve3::Banked(banked) => {
let mut seams = elevated_seams(&banked.base)?;
seams.extend(
banked
.cant
.seams()
.into_iter()
.chain(banked.pivot.seams())
.map(|s| StationSeam::measured(s, false)),
);
seams
}
_ => Vec::new(),
};
Ok(tidy(seams, length))
}
fn elevated_seams(curve: &Elevated3) -> GeomResult<Vec<StationSeam>> {
let mut seams = match curve.plan.as_ref() {
plan @ (Curve2::Intrinsic(_) | Curve2::Chain(_)) => station_seams2(plan)?,
_ => Vec::new(),
};
seams.extend(
crate::elevation::elevation_seams(&curve.elevation)
.into_iter()
.map(|s| StationSeam::measured(s, false)),
);
Ok(seams)
}
fn not_exact(seams: Vec<StationSeam>) -> GeomResult<Vec<StationSeam>> {
if seams.iter().any(|seam| !seam.exact) {
return Err(GeomError::UnsupportedInput {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
input: "exact seam distances of a B-spline with a corner knot: its arc length to \
the knot is a quadrature",
});
}
Ok(seams)
}
fn chain_hides_corners(curve: &Curve2) -> bool {
match curve {
Curve2::Chain(chain) => chain.pieces.iter().any(|piece| match piece {
ChainPiece2::Parametric { curve, .. } => {
!crate::bound::continuity_breaks2(curve, 1).is_empty()
}
_ => false,
}),
_ => false,
}
}
fn hidden_corner() -> GeomError {
GeomError::UnsupportedInput {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
input: "exact seam distances of a chain whose parametric piece turns a corner inside \
itself: no stored length locates it",
}
}
pub fn exact_station_seams2(curve: &Curve2) -> GeomResult<Vec<StationSeam>> {
if chain_hides_corners(curve) {
return Err(hidden_corner());
}
not_exact(station_seams2(curve)?)
}
pub fn exact_station_seams3(curve: &Curve3) -> GeomResult<Vec<StationSeam>> {
let plan = match curve {
Curve3::Elevated(elevated) => Some(elevated.plan.as_ref()),
Curve3::Banked(banked) => Some(banked.base.plan.as_ref()),
_ => None,
};
if plan.is_some_and(chain_hides_corners) {
return Err(hidden_corner());
}
not_exact(station_seams3(curve)?)
}
fn seam_near(seams: Vec<StationSeam>, distance: Scalar) -> Option<StationSeam> {
seams
.into_iter()
.filter(|seam| !seam.smooth && (seam.distance - distance).abs() <= slack(distance))
.min_by(|a, b| {
(a.distance - distance)
.abs()
.total_cmp(&(b.distance - distance).abs())
})
}
fn sided2(curve: &Curve2) -> bool {
matches!(curve, Curve2::Polyline(_) | Curve2::BSpline(_))
}
pub(super) fn sided3(curve: &Curve3) -> bool {
matches!(
curve,
Curve3::Polyline(_) | Curve3::BSpline(_) | Curve3::Elevated(_) | Curve3::Banked(_)
)
}
pub(super) fn seam_section2(
curve: &Curve2,
distance: Scalar,
side: SeamSide,
) -> GeomResult<Option<SectionFrame>> {
if !sided2(curve) {
return Ok(None);
}
let Some(seam) = seam_near(station_seams2(curve)?, distance) else {
return Ok(None);
};
let frame = match curve {
Curve2::Polyline(polyline) => {
let segments = polyline_segments(&polyline.points, polyline.closed, |a: Point2, b| {
(b - a).length()
});
let vertex = seam.parameter as usize;
let (i, j) = polyline_side(&segments, vertex, side)?;
planar_frame(
polyline.points[vertex],
polyline.points[j] - polyline.points[i],
)?
}
Curve2::BSpline(spline) => {
let point = evaluate2(curve, seam.parameter)?;
let tangent = match side {
SeamSide::Incoming => {
let (reversed, pivot) = reversed_spline(spline);
-derivative2(&Curve2::BSpline(reversed), pivot - seam.parameter)?
}
_ => derivative2(curve, seam.parameter)?,
};
planar_frame(point, tangent)?
}
_ => return Ok(None),
};
Ok(Some(frame))
}
pub(crate) fn seam_at3(curve: &Curve3, distance: Scalar) -> GeomResult<Option<StationSeam>> {
if !sided3(curve) {
return Ok(None);
}
Ok(seam_near(station_seams3(curve)?, distance))
}
pub(super) fn seam_section3(
curve: &Curve3,
distance: Scalar,
side: SeamSide,
) -> GeomResult<Option<SectionFrame>> {
let Some(seam) = seam_at3(curve, distance)? else {
return Ok(None);
};
let incoming = side == SeamSide::Incoming;
let at = seam.distance;
let frame = match curve {
Curve3::Polyline(polyline) => {
let segments = polyline_segments(&polyline.points, polyline.closed, |a: Point3, b| {
(b - a).length()
});
let vertex = seam.parameter as usize;
let (i, j) = polyline_side(&segments, vertex, side)?;
reference_up_frame(
polyline.points[vertex],
polyline.points[j] - polyline.points[i],
)?
}
Curve3::BSpline(spline) => {
let point = evaluate3(curve, seam.parameter)?;
let tangent = if incoming {
let (reversed, pivot) = reversed_spline(spline);
-derivative3(&Curve3::BSpline(reversed), pivot - seam.parameter)?
} else {
derivative3(curve, seam.parameter)?
};
reference_up_frame(point, tangent)?
}
Curve3::Elevated(elevated) => {
let truncated;
let read = if incoming {
truncated = Elevated3 {
plan: elevated.plan.clone(),
elevation: elevation_ending_at(&elevated.elevation, at),
};
&truncated
} else {
elevated
};
reference_up_frame(
crate::arc_length::elevated_point(read, at)?,
crate::arc_length::elevated_tangent(read, at)?,
)?
}
Curve3::Banked(banked) => {
let truncated;
let read = if incoming {
truncated = Banked3 {
base: Elevated3 {
plan: banked.base.plan.clone(),
elevation: elevation_ending_at(&banked.base.elevation, at),
},
cant: law_ending_at(&banked.cant, at),
pivot: law_ending_at(&banked.pivot, at),
..banked.clone()
};
&truncated
} else {
banked
};
banked_frame(read, at)?
}
_ => return Ok(None),
};
Ok(Some(frame))
}
fn elevation_ending_at(law: &ElevationLaw, distance: Scalar) -> ElevationLaw {
match law {
ElevationLaw::Piecewise { breaks, laws } if laws.len() == breaks.len() + 1 => {
let keep = breaks.partition_point(|b| *b < distance - slack(distance));
let start = if keep == 0 { 0.0 } else { breaks[keep - 1] };
let mut kept = laws[..keep].to_vec();
kept.push(elevation_ending_at(&laws[keep], distance - start));
ElevationLaw::Piecewise {
breaks: breaks[..keep].to_vec(),
laws: kept,
}
}
other => other.clone(),
}
}
fn law_ending_at(law: &CantLaw, distance: Scalar) -> CantLaw {
let mut start = 0.0;
let mut pieces = Vec::with_capacity(law.pieces.len());
for piece in &law.pieces {
if !pieces.is_empty() && start >= distance - slack(distance) {
break;
}
pieces.push(piece.clone());
start += piece.length;
}
CantLaw::new(pieces)
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mitre {
pub incoming: SectionFrame,
pub outgoing: SectionFrame,
pub normal: Vec3,
}
impl Mitre {
pub fn between(incoming: &SectionFrame, outgoing: &SectionFrame) -> GeomResult<Option<Self>> {
let (t_in, t_out) = (incoming.tangent, outgoing.tangent);
let turn = t_in.cross(t_out).length().atan2(t_in.dot(t_out));
if !turn.is_finite() {
return Err(invalid("station: a seam's tangents are not finite".into()));
}
if turn <= SEAM_TANGENT_TOLERANCE {
return Ok(None);
}
let sum = t_in + t_out;
let half = 0.5 * sum.length();
if half <= MITRE_TOLERANCE {
return Err(GeomError::Degenerate(format!(
"station: the curve turns back on itself at a seam (by {turn} rad), so a run \
across it has no mitre plane"
)));
}
let gap = (outgoing.point - incoming.point).length();
let scale = incoming.point.abs().max_element().max(1.0);
if gap > ARC_LENGTH_TOLERANCE * scale {
return Err(invalid(format!(
"station: the curve's pieces are {gap} apart at a seam, so a run across it has \
no mitre"
)));
}
Ok(Some(Self {
incoming: *incoming,
outgoing: *outgoing,
normal: sum / (2.0 * half),
}))
}
#[must_use]
pub fn place(&self, incoming: Point3, outgoing: Point3, longitudinal: Scalar) -> Point3 {
let project = |frame: &SectionFrame, point: Point3| {
let height = (point - frame.point).dot(self.normal);
point - frame.tangent * (height / frame.tangent.dot(self.normal))
};
let a = project(&self.incoming, incoming);
let b = project(&self.outgoing, outgoing);
(a + b) * 0.5 + self.normal * longitudinal
}
}