use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Interval, Scalar};
use axiolid_curve::{Curve2, Curve3};
use crate::curve::{derivative2, derivative3, domain2, domain3};
pub const ARC_LENGTH_TOLERANCE: Scalar = 1e-12;
pub const MAX_PANELS: usize = 1 << 14;
const MAX_ITERATIONS: usize = 200;
const NODES: [Scalar; 8] = [
-0.960_289_856_497_536_2,
-0.796_666_477_413_626_7,
-0.525_532_409_916_328_9,
-0.183_434_642_495_649_8,
0.183_434_642_495_649_8,
0.525_532_409_916_328_9,
0.796_666_477_413_626_7,
0.960_289_856_497_536_2,
];
const WEIGHTS: [Scalar; 8] = [
0.101_228_536_290_376_3,
0.222_381_034_453_374_5,
0.313_706_645_877_887_3,
0.362_683_783_378_361_9,
0.362_683_783_378_361_9,
0.313_706_645_877_887_3,
0.222_381_034_453_374_5,
0.101_228_536_290_376_3,
];
fn invalid(detail: String) -> GeomError {
GeomError::InvalidInput(detail)
}
enum Family {
Proportional(Scalar),
Numeric {
domain: Option<(Scalar, Scalar)>,
breaks: Vec<Scalar>,
},
}
const ALL_BREAKS: usize = u16::MAX as usize + 1;
fn bounded(domain: Interval) -> Option<(Scalar, Scalar)> {
Some((domain.start.min(domain.end), domain.start.max(domain.end)))
}
fn family2(curve: &Curve2) -> GeomResult<Family> {
Ok(match curve {
Curve2::Line(line) => {
let speed = line.direction.length();
if !(speed.is_finite() && speed > 0.0) {
return Err(invalid("line has no direction, so no arc length".into()));
}
Family::Proportional(speed)
}
Curve2::Intrinsic(_) | Curve2::Chain(_) => Family::Proportional(1.0),
Curve2::Circle(_) | Curve2::Ellipse(_) | Curve2::Sinusoid(_) => Family::Numeric {
domain: None,
breaks: Vec::new(),
},
_ => Family::Numeric {
domain: bounded(domain2(curve)),
breaks: crate::bound::continuity_breaks2(curve, ALL_BREAKS),
},
})
}
fn family3(curve: &Curve3) -> GeomResult<Family> {
Ok(match curve {
Curve3::Line(line) => {
let speed = line.direction.length();
if !(speed.is_finite() && speed > 0.0) {
return Err(invalid("line has no direction, so no arc length".into()));
}
Family::Proportional(speed)
}
Curve3::Intrinsic(_) => Family::Proportional(1.0),
Curve3::Circle(_) | Curve3::Ellipse(_) => Family::Numeric {
domain: None,
breaks: Vec::new(),
},
_ => Family::Numeric {
domain: bounded(domain3(curve)),
breaks: crate::bound::continuity_breaks3(curve, ALL_BREAKS),
},
})
}
pub fn arc_length2(curve: &Curve2, from: Scalar, to: Scalar) -> GeomResult<Scalar> {
let speed = |t| derivative2(curve, t).map(|d| d.length());
arc_length(&family2(curve)?, &speed, from, to)
}
pub fn arc_length3(curve: &Curve3, from: Scalar, to: Scalar) -> GeomResult<Scalar> {
let speed = |t| derivative3(curve, t).map(|d| d.length());
arc_length(&family3(curve)?, &speed, from, to)
}
pub fn parameter_at_arc_length2(
curve: &Curve2,
from: Scalar,
length: Scalar,
) -> GeomResult<Scalar> {
let speed = |t| derivative2(curve, t).map(|d| d.length());
parameter_at(&family2(curve)?, &speed, from, length)
}
pub fn parameter_at_arc_length3(
curve: &Curve3,
from: Scalar,
length: Scalar,
) -> GeomResult<Scalar> {
let speed = |t| derivative3(curve, t).map(|d| d.length());
parameter_at(&family3(curve)?, &speed, from, length)
}
fn tolerance_for(length: Scalar) -> Scalar {
ARC_LENGTH_TOLERANCE * length.abs().max(1.0)
}
fn check_finite(from: Scalar, other: Scalar, what: &str) -> GeomResult<()> {
if from.is_finite() && other.is_finite() {
Ok(())
} else {
Err(invalid(format!(
"arc length needs a finite start parameter and {what}"
)))
}
}
fn inside(domain: Option<(Scalar, Scalar)>, t: Scalar) -> bool {
domain.is_none_or(|(lo, hi)| {
let slack = 1e-12 * (hi - lo).abs().max(1.0);
t >= lo - slack && t <= hi + slack
})
}
fn arc_length<F>(family: &Family, speed: &F, from: Scalar, to: Scalar) -> GeomResult<Scalar>
where
F: Fn(Scalar) -> GeomResult<Scalar>,
{
check_finite(from, to, "a finite end parameter")?;
match family {
Family::Proportional(rate) => Ok(rate * (to - from)),
Family::Numeric { domain, breaks } => {
if !inside(*domain, from) || !inside(*domain, to) {
return Err(invalid(format!(
"arc length from parameter {from} to {to} leaves the curve's domain"
)));
}
let (lo, hi) = (from.min(to), from.max(to));
let magnitude = integrate(speed, breaks, lo, hi)?;
Ok(if to < from { -magnitude } else { magnitude })
}
}
}
fn integrate<F>(speed: &F, breaks: &[Scalar], lo: Scalar, hi: Scalar) -> GeomResult<Scalar>
where
F: Fn(Scalar) -> GeomResult<Scalar>,
{
if hi <= lo {
return Ok(0.0);
}
let mut cuts = vec![lo];
cuts.extend(breaks.iter().copied().filter(|&t| t > lo && t < hi));
cuts.push(hi);
let mut coarse = 0.0;
for w in cuts.windows(2) {
coarse += rule(speed, w[0], w[1])?;
}
let tolerance = tolerance_for(coarse);
let mut total = 0.0;
let mut panels = 0usize;
for w in cuts.windows(2) {
let share = tolerance * (w[1] - w[0]) / (hi - lo);
total += adaptive(speed, w[0], w[1], share, &mut panels)?;
}
if !total.is_finite() {
return Err(invalid(
"curve speed does not integrate to a finite arc length".into(),
));
}
Ok(total)
}
fn rule<F>(speed: &F, a: Scalar, b: Scalar) -> GeomResult<Scalar>
where
F: Fn(Scalar) -> GeomResult<Scalar>,
{
let half = 0.5 * (b - a);
let mid = 0.5 * (a + b);
let mut sum = 0.0;
for (node, weight) in NODES.iter().zip(WEIGHTS.iter()) {
let value = speed(mid + half * node)?;
if !value.is_finite() {
return Err(invalid(format!(
"curve speed is not finite at parameter {}",
mid + half * node
)));
}
sum += weight * value;
}
Ok(sum * half)
}
fn adaptive<F>(
speed: &F,
a: Scalar,
b: Scalar,
tolerance: Scalar,
panels: &mut usize,
) -> GeomResult<Scalar>
where
F: Fn(Scalar) -> GeomResult<Scalar>,
{
let width = b - a;
let mut stack = vec![(a, b, rule(speed, a, b)?)];
let mut total = 0.0;
while let Some((lo, hi, whole)) = stack.pop() {
*panels += 1;
if *panels > MAX_PANELS {
return Err(GeomError::BudgetExceeded {
resource: "arc-length quadrature panels",
});
}
let mid = 0.5 * (lo + hi);
let left = rule(speed, lo, mid)?;
let right = rule(speed, mid, hi)?;
let share = tolerance * (hi - lo) / width;
if (left + right - whole).abs() <= share || !(mid > lo && mid < hi) {
total += left + right;
} else {
stack.push((mid, hi, right));
stack.push((lo, mid, left));
}
}
Ok(total)
}
fn parameter_at<F>(family: &Family, speed: &F, from: Scalar, length: Scalar) -> GeomResult<Scalar>
where
F: Fn(Scalar) -> GeomResult<Scalar>,
{
check_finite(from, length, "a finite arc length")?;
let (domain, breaks) = match family {
Family::Proportional(rate) => return Ok(from + length / rate),
Family::Numeric { domain, breaks } => (*domain, breaks),
};
if !inside(domain, from) {
return Err(invalid(format!(
"arc length is measured from parameter {from}, outside the curve's domain"
)));
}
if length == 0.0 {
return Ok(from);
}
let direction = length.signum();
let target = length.abs();
let tolerance = tolerance_for(target);
let travelled = |x: Scalar| -> GeomResult<Scalar> {
let to = from + direction * x;
integrate(speed, breaks, from.min(to), from.max(to))
};
let (mut hi, mut at_hi) = match domain {
Some((lo, hi)) => {
let reach = if direction > 0.0 {
hi - from
} else {
from - lo
}
.max(0.0);
let available = travelled(reach)?;
if available < target - tolerance {
return Err(invalid(format!(
"arc length {target} exceeds the {available} available along the curve \
from parameter {from}"
)));
}
if available <= target {
return Ok(from + direction * reach);
}
(reach, available)
}
None => {
let mut reach: Scalar = 1.0;
let mut grown = 0;
loop {
let available = travelled(reach)?;
if available >= target {
break (reach, available);
}
grown += 1;
if grown > 64 {
return Err(invalid(format!(
"arc length {target} is not reached along the curve from parameter {from}"
)));
}
reach *= 2.0;
}
}
};
let mut lo = 0.0;
let mut at_lo = 0.0;
let mut x = hi * target / at_hi;
for _ in 0..MAX_ITERATIONS {
if !(x > lo && x < hi) {
x = 0.5 * (lo + hi);
}
let g = travelled(x)?;
let residual = g - target;
if residual.abs() <= tolerance {
let rate = speed(from + direction * x)?;
let polished = x - residual / rate;
let x = if rate.is_finite() && rate > 0.0 && polished >= lo && polished <= hi {
polished
} else {
x
};
return Ok(from + direction * x);
}
if residual < 0.0 {
lo = x;
at_lo = g;
} else {
hi = x;
at_hi = g;
}
let midpoint = 0.5 * (lo + hi);
if !(midpoint > lo && midpoint < hi) {
return Ok(from + direction * x);
}
let rate = speed(from + direction * x)?;
let newton = if rate.is_finite() && rate > 0.0 {
x - residual / rate
} else {
Scalar::NAN
};
x = if newton > lo && newton < hi {
newton
} else {
let secant = lo + (target - at_lo) * (hi - lo) / (at_hi - at_lo);
if secant > lo && secant < hi {
secant
} else {
midpoint
}
};
}
Err(GeomError::BudgetExceeded {
resource: "arc-length inverse iterations",
})
}