use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Point3, Scalar, Transform3};
use axiolid_curve::{
Curve2, Curve3, CurvePath, PathBound, PathCurve, PathOffset, PathPiece, SeamSide,
};
use axiolid_curve_evaluate_contract::{window_span, DistanceConvention};
use super::certify::{
checked_tolerance, curve_span, piece_length, CertifiedSeam, DistanceInterval, Running,
POINT_ROUNDING, UNCERTIFIED_LENGTH,
};
use super::offset::{OffsetTable, StationOffset};
use super::seam::tidy;
use super::window::{certification, checked_window, snap};
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;
const CHORD_ROUNDING: Scalar = 100.0 * POINT_ROUNDING;
const TURN_SLACK: 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),
Offset(StationOffset<'c>),
}
impl From<StationCurve<'_>> for PathCurve {
fn from(curve: StationCurve<'_>) -> Self {
match curve {
StationCurve::Two(curve) => PathCurve::Two(curve.clone()),
StationCurve::Three(curve) => PathCurve::Three(curve.clone()),
StationCurve::Offset(offset) => PathCurve::Offset(Box::new(PathOffset::new(
PathPiece::from(offset.base()),
offset.law(),
))),
}
}
}
impl<'c> StationCurve<'c> {
pub fn from_path_curve(curve: &'c PathCurve) -> GeomResult<Self> {
match curve {
PathCurve::Two(curve) => Ok(Self::Two(curve)),
PathCurve::Three(curve) => Ok(Self::Three(curve)),
PathCurve::Offset(offset) => Ok(Self::Offset(StationOffset::new(
StationPiece::from_path_piece(&offset.base)?,
offset.law,
)?)),
_ => Err(unsupported(
"a curve path piece of a kind this evaluator does not know",
)),
}
}
}
impl StationCurve<'_> {
#[must_use]
pub fn convention(self) -> DistanceConvention {
match self {
Self::Three(Curve3::Elevated(_) | Curve3::Banked(_)) => {
DistanceConvention::PlanDistance
}
Self::Offset(offset) => offset.convention(),
_ => DistanceConvention::ArcLength3d,
}
}
fn length(self) -> GeomResult<Option<Scalar>> {
match self {
Self::Two(curve) => station_length2(curve),
Self::Three(curve) => station_length3(curve),
Self::Offset(offset) => offset.length().map(Some),
}
}
fn frame_is_exact(self) -> bool {
match self {
Self::Offset(offset) => offset.frame_is_exact(),
_ => self.is_line(),
}
}
fn end(self) -> GeomResult<Scalar> {
match self.line_speed() {
Some(speed) => Ok(speed),
None => self
.length()?
.ok_or_else(|| invalid("station: a composite piece has no bounded length".into())),
}
}
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(_))
)
}
pub(super) fn measure_is_exact(self) -> bool {
match self {
Self::Offset(offset) => offset.measure_is_exact(),
_ => !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 turned(self, t: Scalar) -> Scalar {
if self.is_closed_conic() {
t.rem_euclid(core::f64::consts::TAU)
} else {
t
}
}
fn wound(self, t0: Scalar, t1: Scalar) -> (Scalar, Scalar) {
if !self.is_closed_conic() {
return (t0, t1);
}
let turn = core::f64::consts::TAU;
let travelled = t1 - t0;
let span = if travelled > 0.0 && travelled <= turn * (1.0 + TURN_SLACK) {
travelled
} else {
match travelled.rem_euclid(turn) {
0.0 => turn,
wrapped => wrapped,
}
};
(t0, t0 + span)
}
fn measure_after(self, t: Scalar, start: Scalar) -> GeomResult<Scalar> {
let m = self.measure_at(self.turned(t))?;
if self.is_closed_conic() && m <= start {
return Ok(self.length()?.unwrap_or(Scalar::INFINITY) + m);
}
Ok(m)
}
pub fn parameter_at_bound(self, bound: PathBound) -> GeomResult<Scalar> {
super::path_bound::bound_parameter(self, bound)
}
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)?,
Self::Offset(_) => {
return Err(unsupported(
"the native parameter of an offset piece: an offset takes its basis's \
parameter, not its own length",
))
}
_ => t,
})
}
fn point_at(self, m: Scalar, table: Option<&OffsetTable>) -> GeomResult<Point3> {
let m = self.folded(m)?;
match self {
Self::Offset(offset) => offset.point_at(m, table),
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,
table: Option<&OffsetTable>,
) -> GeomResult<SectionFrame> {
let m = self.folded(m)?;
match self {
Self::Offset(offset) => offset.section_at(m, table),
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),
Self::Offset(_) => Ok(Vec::new()),
}
}
}
#[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> {
Self::between(curve, 0.0, curve.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
}
pub fn offset(offset: StationOffset<'c>) -> GeomResult<Self> {
let end = offset.length()?;
if !(end.is_finite() && end > 0.0) {
return Err(GeomError::Degenerate(
"station: an offset piece has no length".into(),
));
}
Ok(Self {
curve: StationCurve::Offset(offset),
start: 0.0,
end,
reversed: false,
placement: None,
placement_exact: true,
})
}
pub(super) fn table(&self) -> GeomResult<Option<OffsetTable>> {
match self.curve {
StationCurve::Offset(offset) => offset.table().map(Some),
_ => Ok(None),
}
}
pub fn from_path_piece(piece: &'c PathPiece) -> GeomResult<Self> {
let curve = StationCurve::from_path_curve(&piece.curve)?;
if piece.open_end && piece.end_bound.is_some() {
return Err(invalid(
"station: a curve path piece whose end is both open and bounded by a parameter \
or a point"
.into(),
));
}
let t0 = piece
.start_bound
.map(|bound| curve.parameter_at_bound(bound))
.transpose()?;
let t1 = piece
.end_bound
.map(|bound| curve.parameter_at_bound(bound))
.transpose()?;
let span = match (t0, t1) {
(Some(t0), Some(t1)) => {
let (t0, t1) = curve.wound(t0, t1);
Self::between_parameters(curve, t0, t1)?
}
_ => {
let start = match t0 {
Some(t) => curve.measure_at(curve.turned(t))?,
None => piece.start,
};
let end = if piece.open_end {
curve.end()?
} else if let Some(t) = t1 {
curve.measure_after(t, start)?
} else {
piece.end
};
Self::between(curve, start, end)?
}
};
Ok(Self {
reversed: piece.reversed,
placement: piece.placement,
placement_exact: piece.placement_exact,
..span
})
}
#[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.frame_is_exact() && 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, table: Option<&OffsetTable>) -> GeomResult<Point3> {
let (m, _) = self.measure(u, SeamSide::Outgoing);
let point = self.curve.point_at(m, table)?;
Ok(match self.placement {
Some(rigid) => rigid.transform_point3(point),
None => point,
})
}
fn length_floor(&self, table: Option<&OffsetTable>) -> GeomResult<Scalar> {
let a = self.curve.point_at(self.start, table)?;
let b = self.curve.point_at(self.end, table)?;
let d = b - a;
let chord = match self.convention() {
DistanceConvention::PlanDistance => d.x.hypot(d.y),
_ => d.length(),
};
let scale = a
.abs()
.max_element()
.max(b.abs().max_element())
.max(self.length())
.max(1.0);
Ok((chord - CHORD_ROUNDING * scale).max(0.0))
}
pub fn section_on(&self, u: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
self.section_with(u, side, None)
}
fn section_with(
&self,
u: Scalar,
side: SeamSide,
table: Option<&OffsetTable>,
) -> 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, table)?;
if self.reversed {
section.tangent = -section.tangent;
section.lateral = -section.lateral;
}
match self.placement {
Some(rigid) => section.carried(rigid, self.convention()),
None => Ok(section),
}
}
pub(super) 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)
}
}
impl From<StationPiece<'_>> for PathPiece {
fn from(piece: StationPiece<'_>) -> Self {
let mut out = PathPiece::new(piece.curve.into(), piece.start, piece.end);
out.reversed = piece.reversed;
out.placement = piece.placement;
out.placement_exact = piece.placement_exact;
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompositeBasis<'c> {
pieces: Vec<StationPiece<'c>>,
tables: Vec<Option<OffsetTable>>,
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",
));
}
}
let tables = pieces
.iter()
.map(StationPiece::table)
.collect::<GeomResult<Vec<_>>>()?;
for index in 1..pieces.len() {
check_joint(
(&pieces[index - 1], tables[index - 1].as_ref()),
(&pieces[index], tables[index].as_ref()),
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,
tables,
starts,
convention,
})
}
pub fn from_path(path: &'c CurvePath) -> GeomResult<Self> {
let pieces = path
.pieces()
.iter()
.map(StationPiece::from_path_piece)
.collect::<GeomResult<Vec<_>>>()?;
Self::new(pieces)
}
#[must_use]
pub fn path(&self) -> CurvePath {
self.pieces
.iter()
.map(|piece| PathPiece::from(*piece))
.collect()
}
#[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(crate) fn piece_read(
&self,
distance: Scalar,
side: SeamSide,
) -> GeomResult<&StationPiece<'c>> {
let distance = admitted(distance, Some(self.length()))?;
Ok(&self.pieces[self.locate(distance, side).0])
}
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_with(into, side, self.tables[piece].as_ref())
}
#[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)
}
fn shares(&self, tolerance: Scalar) -> Vec<Scalar> {
self.shares_until(tolerance, Scalar::INFINITY)
}
fn shares_until(&self, tolerance: Scalar, until: Scalar) -> Vec<Scalar> {
let numeric: Scalar = self
.pieces
.iter()
.zip(&self.starts)
.filter(|(_, start)| **start <= until)
.map(|(piece, _)| piece)
.filter(|piece| !piece.curve.measure_is_exact())
.map(StationPiece::length)
.sum();
self.pieces
.iter()
.map(|piece| {
if numeric > 0.0 {
0.9 * tolerance * piece.length() / numeric
} else {
tolerance
}
})
.collect()
}
pub fn joint_intervals(&self, tolerance: Scalar) -> GeomResult<Vec<DistanceInterval>> {
let tolerance = checked_tolerance(tolerance)?;
Ok(self
.certified_joints(tolerance, None)?
.into_iter()
.map(|(_, interval)| interval)
.collect())
}
pub fn joint_intervals_within(
&self,
around: Scalar,
window: Scalar,
tolerance: Scalar,
) -> GeomResult<Vec<(usize, DistanceInterval)>> {
let tolerance = checked_tolerance(tolerance)?;
let span = window_span(around, window)?;
self.certified_joints(tolerance, Some(span))
}
fn certified_joints(
&self,
tolerance: Scalar,
span: Option<DistanceInterval>,
) -> GeomResult<Vec<(usize, DistanceInterval)>> {
let shares = self.shares(tolerance);
let joints = self.pieces.len().saturating_sub(1);
let mut run = Running::ZERO;
let mut out = Vec::with_capacity(joints);
for (k, ((piece, table), share)) in self
.pieces
.iter()
.zip(&self.tables)
.zip(shares)
.take(joints)
.enumerate()
{
if let Some(span) = span {
let floor = (run.interval().lower + piece.length_floor(table.as_ref())?)
* (1.0 - 4.0 * Scalar::EPSILON);
if floor > span.upper {
break;
}
}
run = run.plus(piece_length(piece, table.as_ref(), share)?);
let interval = run.interval();
if interval.width().is_nan() || interval.width() > tolerance {
return Err(unsupported(UNCERTIFIED_LENGTH));
}
if span.is_none_or(|span| interval.meets(&span)) {
out.push((k, interval));
}
}
Ok(out)
}
pub(crate) fn certified_seams(
&self,
tolerance: Scalar,
until: Scalar,
) -> GeomResult<Vec<CertifiedSeam>> {
let shares = self.shares_until(tolerance, until);
let mut run = Running::ZERO;
let mut out = Vec::new();
for (k, piece) in self.pieces.iter().enumerate() {
if k > 0 {
out.push(CertifiedSeam {
distance: self.starts[k],
interval: run.interval(),
});
}
if run.interval().lower > until {
break;
}
for seam in piece.seams()? {
if seam.smooth {
continue;
}
let part = if seam.exact {
DistanceInterval::point(seam.distance)
} else {
let u = seam.distance;
let (m0, m1) = if piece.reversed {
(piece.end - u, piece.end)
} else {
(piece.start, piece.start + u)
};
curve_span(piece.curve, m0, m1, shares[k])?
};
out.push(CertifiedSeam {
distance: self.starts[k] + seam.distance,
interval: run.plus(part).interval(),
});
}
if k + 1 < self.pieces.len() {
run = run.plus(piece_length(piece, self.tables[k].as_ref(), shares[k])?);
}
}
Ok(out)
}
pub fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
if checked_window(window)? == 0.0 || !distance.is_finite() {
return Ok(distance);
}
let seams = self.certified_seams(certification(window), distance + window)?;
snap(&seams, distance, window)
}
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)
}
}
type Read<'a, 'c> = (&'a StationPiece<'c>, Option<&'a OffsetTable>);
fn check_joint(
(before, before_table): Read<'_, '_>,
(after, after_table): Read<'_, '_>,
index: usize,
) -> GeomResult<()> {
let end = before.point_at(before.length(), before_table)?;
let start = after.point_at(0.0, after_table)?;
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(());
}
if [before, after]
.iter()
.any(|piece| matches!(piece.curve, StationCurve::Offset(_)))
{
return Err(invalid(format!(
"station: an offset across a corner of its basis at joint {index}: the offsets of \
the two sides do not meet (a gap of {}), so no distance runs across it (the \
basis's tangent or roll jumps there)",
(end - start).length()
)));
}
let after_end = after.point_at(after.length(), after_table)?;
let reversed = meets(end, after_end)
|| (index == 1 && {
let before_start = before.point_at(0.0, before_table)?;
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()
)))
}