use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
use axiolid_curve::{Curve2, Curve3, PathBound};
pub use axiolid_curve_evaluate_contract::{PATH_POINT_AMBIGUOUS, PATH_POINT_OFF_CURVE};
use super::composite::{StationCurve, JOINT_TOLERANCE};
use crate::curve::{
derivative2, derivative3, domain2, domain3, evaluate2, evaluate3, invert2, invert3,
second_derivative2, second_derivative3,
};
pub const POINT_BOUND_UNSUPPORTED: &str =
"a curve path piece bounded by a point on a curve this evaluator does not invert (an \
intrinsic curve, a chain, an elevated or banked curve, an offset, or a family other than a \
line, a circle, an ellipse, a polyline or a B-spline): state its parameter instead";
const SAMPLES_PER_CONTROL_POINT: usize = 64;
const NEWTON_STEPS: usize = 60;
fn refused(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
input,
}
}
#[must_use]
pub fn point_bound_tolerance(point: Point3) -> Scalar {
JOINT_TOLERANCE * point.abs().max_element().max(1.0)
}
pub(super) fn bound_parameter(curve: StationCurve<'_>, bound: PathBound) -> GeomResult<Scalar> {
match bound {
PathBound::Parameter(t) if t.is_finite() => Ok(t),
PathBound::Parameter(t) => Err(GeomError::Degenerate(format!(
"station: a curve path piece is bounded at parameter {t}, which is not finite"
))),
PathBound::Point(point) => point_parameter(curve, point),
_ => Err(refused(
"a curve path piece bound of a kind this evaluator does not know",
)),
}
}
fn point_parameter(curve: StationCurve<'_>, point: Point3) -> GeomResult<Scalar> {
if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
return Err(GeomError::InvalidInput(format!(
"station: a curve path piece is bounded at point {point:?}, which is not finite"
)));
}
let traced = Traced::of(curve)?;
let found = traced.parameters(point, point_bound_tolerance(point))?;
match found.as_slice() {
[] => Err(refused(PATH_POINT_OFF_CURVE)),
[t] => Ok(*t),
_ => Err(refused(PATH_POINT_AMBIGUOUS)),
}
}
#[derive(Clone, Copy)]
enum Traced<'c> {
Two(&'c Curve2),
Three(&'c Curve3),
}
impl<'c> Traced<'c> {
fn of(curve: StationCurve<'c>) -> GeomResult<Self> {
match curve {
StationCurve::Two(
curve @ (Curve2::Line(_)
| Curve2::Circle(_)
| Curve2::Ellipse(_)
| Curve2::Polyline(_)
| Curve2::BSpline(_)),
) => Ok(Self::Two(curve)),
StationCurve::Three(
curve @ (Curve3::Line(_)
| Curve3::Circle(_)
| Curve3::Ellipse(_)
| Curve3::Polyline(_)
| Curve3::BSpline(_)),
) => Ok(Self::Three(curve)),
_ => Err(refused(POINT_BOUND_UNSUPPORTED)),
}
}
fn point(self, t: Scalar) -> GeomResult<Point3> {
match self {
Self::Two(curve) => {
let p = evaluate2(curve, t)?;
Ok(Point3::new(p.x, p.y, 0.0))
}
Self::Three(curve) => evaluate3(curve, t),
}
}
fn derivatives(self, t: Scalar) -> GeomResult<(Vec3, Vec3)> {
match self {
Self::Two(curve) => {
let (d, dd) = (derivative2(curve, t)?, second_derivative2(curve, t)?);
Ok((Vec3::new(d.x, d.y, 0.0), Vec3::new(dd.x, dd.y, 0.0)))
}
Self::Three(curve) => Ok((derivative3(curve, t)?, second_derivative3(curve, t)?)),
}
}
fn distance(self, t: Scalar, point: Point3) -> GeomResult<Scalar> {
Ok((self.point(t)? - point).length())
}
fn parameters(self, point: Point3, tolerance: Scalar) -> GeomResult<Vec<Scalar>> {
match self {
Self::Two(Curve2::Polyline(polyline)) => {
let points: Vec<Point3> = polyline
.points
.iter()
.map(|p| Point3::new(p.x, p.y, 0.0))
.collect();
Ok(polyline_parameters(
&points,
polyline.closed,
point,
tolerance,
))
}
Self::Three(Curve3::Polyline(polyline)) => Ok(polyline_parameters(
&polyline.points,
polyline.closed,
point,
tolerance,
)),
Self::Two(Curve2::BSpline(_)) | Self::Three(Curve3::BSpline(_)) => {
self.spline_parameters(point, tolerance)
}
_ => {
let anywhere = Tolerance::new(Scalar::MAX, 0.0).map_err(|_| {
GeomError::InvalidInput("station: no inversion tolerance".into())
})?;
let t = match self {
Self::Two(curve) => invert2(curve, Point2::new(point.x, point.y), anywhere)?,
Self::Three(curve) => invert3(curve, point, anywhere)?,
};
Ok(if self.distance(t, point)? <= tolerance {
vec![t]
} else {
Vec::new()
})
}
}
}
fn spline_parameters(self, point: Point3, tolerance: Scalar) -> GeomResult<Vec<Scalar>> {
let (domain, count, knots) = match self {
Self::Two(curve @ Curve2::BSpline(spline)) => (
domain2(curve),
spline.control_points.len(),
spline.knots.as_slice(),
),
Self::Three(curve @ Curve3::BSpline(spline)) => (
domain3(curve),
spline.control_points.len(),
spline.knots.as_slice(),
),
_ => return Err(refused(POINT_BOUND_UNSUPPORTED)),
};
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
if !(lo.is_finite() && hi.is_finite() && hi > lo) {
return Err(GeomError::Degenerate(
"station: a B-spline with no parameter domain bounds a curve path piece".into(),
));
}
let samples = count.max(2) * SAMPLES_PER_CONTROL_POINT;
let mut at: Vec<Scalar> = (0..=samples)
.map(|i| lo + (hi - lo) * i as Scalar / samples as Scalar)
.chain(knots.iter().copied().filter(|&k| k > lo && k < hi))
.collect();
at.sort_by(Scalar::total_cmp);
at.dedup();
let off = at
.iter()
.map(|&t| self.distance(t, point))
.collect::<GeomResult<Vec<_>>>()?;
let last = at.len() - 1;
let mut found = Vec::new();
for i in 0..=last {
let falls = i == 0 || off[i] <= off[i - 1];
let rises = i == last || off[i] <= off[i + 1];
if !(falls && rises) {
continue;
}
let (a, b) = (at[i.saturating_sub(1)], at[(i + 1).min(last)]);
let refined = self.refine(at[i], a, b, point)?;
let near = self.distance(refined, point)?;
let (t, distance) = if near <= off[i] {
(refined, near)
} else {
(at[i], off[i])
};
if distance <= tolerance {
found.push((t, distance));
}
}
found.sort_by(|x, y| x.0.total_cmp(&y.0));
places(found, |ta, tb| {
let between = at
.iter()
.zip(&off)
.filter(|(t, _)| **t > ta && **t < tb)
.all(|(_, d)| *d <= tolerance);
Ok(between && self.distance(0.5 * (ta + tb), point)? <= tolerance)
})
}
fn refine(self, mut t: Scalar, a: Scalar, b: Scalar, point: Point3) -> GeomResult<Scalar> {
for _ in 0..NEWTON_STEPS {
let (d, dd) = self.derivatives(t)?;
let r = self.point(t)? - point;
let (g, h) = (r.dot(d), d.dot(d) + r.dot(dd));
if !(h.is_finite() && h > 0.0) {
break;
}
let next = (t - g / h).clamp(a, b);
let moved = (next - t).abs();
t = next;
if moved <= 4.0 * Scalar::EPSILON * (1.0 + t.abs()) {
break;
}
}
Ok(t)
}
}
fn polyline_parameters(
points: &[Point3],
closed: bool,
point: Point3,
tolerance: Scalar,
) -> Vec<Scalar> {
let n = points.len();
let segments = if closed { n } else { n.saturating_sub(1) };
let mut found = Vec::new();
for i in 0..segments {
let (a, b) = (points[i], points[(i + 1) % n]);
let d = b - a;
let dd = d.dot(d);
let u = if dd > 0.0 {
((point - a).dot(d) / dd).clamp(0.0, 1.0)
} else {
0.0
};
let distance = (a + d * u - point).length();
if distance <= tolerance {
found.push((i as Scalar + u, distance));
}
}
let vertex = |k: usize| points[k % n.max(1)];
places(found, |ta, tb| {
let first = ta.floor() as usize + 1;
let last = tb.ceil() as usize;
Ok((first..last)
.filter(|&k| (k as Scalar) > ta && (k as Scalar) < tb)
.all(|k| (vertex(k) - point).length() <= tolerance))
})
.unwrap_or_default()
}
fn places(
found: Vec<(Scalar, Scalar)>,
same: impl Fn(Scalar, Scalar) -> GeomResult<bool>,
) -> GeomResult<Vec<Scalar>> {
let mut out: Vec<(Scalar, Scalar)> = Vec::new();
let mut previous: Option<Scalar> = None;
for (t, distance) in found {
let joins = match previous {
Some(before) => same(before, t)?,
None => false,
};
previous = Some(t);
match out.last_mut() {
Some(place) if joins => {
if distance < place.1 {
*place = (t, distance);
}
}
_ => out.push((t, distance)),
}
}
Ok(out.into_iter().map(|(t, _)| t).collect())
}