use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame2, Frame3, Interval, Point2, Point3, Scalar, Tolerance, Vec2, Vec3};
use axiolid_curve::{
BSplineCurve, BSplineCurve2, BSplineCurve3, Circle2, Circle3, Curve2, Curve3, CurveEvaluator,
Ellipse2, Ellipse3, Line2, Line3, Polyline2, Polyline3,
};
use crate::nurbs::SplineAxis;
#[derive(Debug, Clone, Copy, Default)]
pub struct ScalarCurve;
impl ScalarCurve {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurveJet<P, D> {
pub point: P,
pub first: D,
pub second: D,
}
#[must_use]
pub fn domain2(curve: &Curve2) -> Interval {
match curve {
Curve2::Line(_) => Interval::UNIT,
Curve2::Circle(_) | Curve2::Ellipse(_) => full_turn(),
Curve2::Polyline(p) => polyline_domain(p.points.len(), p.closed),
Curve2::BSpline(b) => spline_domain(b),
Curve2::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
start: 0.0,
end: i.length,
},
Curve2::Sinusoid(_) => full_turn(),
_ => Interval {
start: 0.0,
end: 0.0,
},
}
}
#[must_use]
pub fn domain3(curve: &Curve3) -> Interval {
match curve {
Curve3::Line(_) => Interval::UNIT,
Curve3::Circle(_) | Curve3::Ellipse(_) => full_turn(),
Curve3::Polyline(p) => polyline_domain(p.points.len(), p.closed),
Curve3::BSpline(b) => spline_domain(b),
Curve3::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
start: 0.0,
end: i.length,
},
_ => Interval {
start: 0.0,
end: 0.0,
},
}
}
fn full_turn() -> Interval {
Interval {
start: 0.0,
end: core::f64::consts::TAU,
}
}
fn polyline_domain(count: usize, closed: bool) -> Interval {
let segments = if closed {
count
} else {
count.saturating_sub(1)
};
Interval {
start: 0.0,
end: segments as Scalar,
}
}
fn spline_domain<P>(b: &BSplineCurve<P>) -> Interval {
SplineAxis::new(
&b.knots,
&b.multiplicities,
b.degree,
b.control_points.len(),
"B-spline curve",
)
.map_or(
Interval {
start: 0.0,
end: 0.0,
},
|axis| {
let (start, end) = axis.domain();
Interval { start, end }
},
)
}
pub fn evaluate2(curve: &Curve2, t: Scalar) -> GeomResult<Point2> {
finite(t)?;
let value = match curve {
Curve2::Line(l) => Ok(line_point(l.origin, l.direction, t)),
Curve2::Circle(c) => Ok(conic_point2(&c.frame, c.radius, c.radius, t)),
Curve2::Ellipse(e) => Ok(conic_point2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve2::Polyline(p) => polyline_point(&p.points, p.closed, t),
Curve2::BSpline(b) => de_boor(b, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1])),
Curve2::Intrinsic(i) => crate::arc_length::intrinsic_point(i, t),
Curve2::Sinusoid(w) => Ok(Point2::new(t, w.height(t))),
Curve2::QuadraticGraph(g) => g
.height(t)
.map(|v| Point2::new(t, v))
.ok_or_else(|| outside_graph(t)),
Curve2::AngleGraph(g) => g
.angle(t)
.map(|u| Point2::new(u, t))
.ok_or_else(|| outside_graph(t)),
Curve2::Implicit(c) => c.point(t).ok_or_else(|| outside_graph(t)),
Curve2::Lifted(l) => {
if let Some((section, first)) = pair_side(l) {
let (a, b, _) = section.solve(t).ok_or_else(|| outside_graph(t))?;
return finite2(if first { a } else { b }, "curve point");
}
let p = evaluate3(&l.curve, t)?;
l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))
}
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite2(value, "curve point")
}
pub fn derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
finite(t)?;
let value = match curve {
Curve2::Line(l) => Ok(l.direction),
Curve2::Circle(c) => Ok(conic_tangent2(&c.frame, c.radius, c.radius, t)),
Curve2::Ellipse(e) => Ok(conic_tangent2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve2::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
Curve2::BSpline(b) => de_boor_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1])),
Curve2::Intrinsic(i) => crate::arc_length::intrinsic_tangent(i, t),
Curve2::Sinusoid(w) => {
let (sin, cos) = t.sin_cos();
Ok(Vec2::new(1.0, -w.cosine * sin + w.sine * cos))
}
Curve2::QuadraticGraph(g) => g
.slope(t)
.map(|slope| Vec2::new(1.0, slope))
.ok_or_else(|| outside_graph(t)),
Curve2::AngleGraph(g) => g
.slope(t)
.map(|slope| Vec2::new(slope, 1.0))
.ok_or_else(|| outside_graph(t)),
Curve2::Implicit(c) => c.derivative(t).ok_or_else(|| outside_graph(t)),
Curve2::Lifted(l) => lifted_rates(l, t).map(|(d, _)| d),
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite2(value, "curve derivative")
}
pub fn second_derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
finite(t)?;
let value = match curve {
Curve2::Line(_) | Curve2::Polyline(_) => Ok(Vec2::ZERO),
Curve2::Circle(c) => Ok(conic_second2(&c.frame, c.radius, c.radius, t)),
Curve2::Ellipse(e) => Ok(conic_second2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve2::BSpline(b) => {
de_boor_second_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))
}
Curve2::Sinusoid(w) => {
let (sin, cos) = t.sin_cos();
Ok(Vec2::new(0.0, -w.cosine * cos - w.sine * sin))
}
Curve2::QuadraticGraph(g) => g
.bend(t)
.map(|bend| Vec2::new(0.0, bend))
.ok_or_else(|| outside_graph(t)),
Curve2::AngleGraph(g) => g
.bend(t)
.map(|bend| Vec2::new(bend, 0.0))
.ok_or_else(|| outside_graph(t)),
Curve2::Implicit(c) => c.second_derivative(t).ok_or_else(|| outside_graph(t)),
Curve2::Lifted(l) => lifted_rates(l, t).map(|(_, dd)| dd),
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite2(value, "curve second derivative")
}
pub fn bspline_jet2(curve: &BSplineCurve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
Ok(CurveJet {
point: de_boor(curve, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1]))?,
first: de_boor_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
second: de_boor_second_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
})
}
pub fn jet2(curve: &Curve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
Ok(CurveJet {
point: evaluate2(curve, t)?,
first: derivative2(curve, t)?,
second: second_derivative2(curve, t)?,
})
}
fn lifted_rates(l: &axiolid_curve::LiftedCurve2, t: Scalar) -> GeomResult<(Vec2, Vec2)> {
if let Some((section, first)) = pair_side(l) {
let (a, b, _) = section.rates(t).ok_or_else(|| outside_graph(t))?;
let (aa, bb, _) = section.second_rates(t).ok_or_else(|| outside_graph(t))?;
let pick = |x: Point2, y: Point2| if first { x } else { y };
let (d, dd) = (pick(a, b), pick(aa, bb));
return Ok((Vec2::new(d.x, d.y), Vec2::new(dd.x, dd.y)));
}
let p = evaluate3(&l.curve, t)?;
let uv = l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))?;
let jet = l.carrier.jet(uv.x, uv.y);
let (a, b, c) = (jet.u.dot(jet.u), jet.u.dot(jet.v), jet.v.dot(jet.v));
let det = a * c - b * b;
if det == 0.0 || !det.is_finite() {
return Err(outside_graph(t));
}
let solve = |w: Vec3| {
let (g0, g1) = (jet.u.dot(w), jet.v.dot(w));
Vec2::new((c * g0 - b * g1) / det, (a * g1 - b * g0) / det)
};
let d = solve(derivative3(&l.curve, t)?);
let rest = second_derivative3(&l.curve, t)?
- (jet.uu * (d.x * d.x) + jet.uv * (2.0 * d.x * d.y) + jet.vv * (d.y * d.y));
Ok((d, solve(rest)))
}
fn pair_side(l: &axiolid_curve::LiftedCurve2) -> Option<(&axiolid_curve::PairSection3, bool)> {
match l.curve.as_ref() {
Curve3::PairSection(section) => section.side(&l.carrier).map(|first| (section, first)),
_ => None,
}
}
fn outside_graph(t: Scalar) -> GeomError {
GeomError::Degenerate(format!(
"quadratic section graph has no regular point at t = {t}"
))
}
pub fn evaluate3(curve: &Curve3, t: Scalar) -> GeomResult<Point3> {
finite(t)?;
let value = match curve {
Curve3::Line(l) => Ok(line_point(l.origin, l.direction, t)),
Curve3::Circle(c) => Ok(conic_point3(&c.frame, c.radius, c.radius, t)),
Curve3::Ellipse(e) => Ok(conic_point3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve3::Polyline(p) => polyline_point(&p.points, p.closed, t),
Curve3::BSpline(b) => de_boor(b, t, |p| [p.x, p.y, p.z], |c| Point3::new(c[0], c[1], c[2])),
Curve3::Intrinsic(i) => crate::frenet::frenet_point(i, t),
Curve3::RuledSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
Curve3::TorusSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
Curve3::ImplicitSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
Curve3::PairSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite3(value, "curve point")
}
pub fn derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
finite(t)?;
let value = match curve {
Curve3::Line(l) => Ok(l.direction),
Curve3::Circle(c) => Ok(conic_tangent3(&c.frame, c.radius, c.radius, t)),
Curve3::Ellipse(e) => Ok(conic_tangent3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve3::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
Curve3::BSpline(b) => {
de_boor_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
}
Curve3::Intrinsic(i) => crate::frenet::frenet_tangent(i, t),
Curve3::RuledSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
Curve3::TorusSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
Curve3::ImplicitSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
Curve3::PairSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite3(value, "curve derivative")
}
pub fn second_derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
finite(t)?;
let value = match curve {
Curve3::Line(_) | Curve3::Polyline(_) => Ok(Vec3::ZERO),
Curve3::Circle(c) => Ok(conic_second3(&c.frame, c.radius, c.radius, t)),
Curve3::Ellipse(e) => Ok(conic_second3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
Curve3::BSpline(b) => {
de_boor_second_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
}
Curve3::RuledSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
Curve3::TorusSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
Curve3::ImplicitSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
Curve3::PairSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
_ => Err(GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("axiolid-reference"),
operation: axiolid_contracts::Operation::CurveEvaluation,
}),
}?;
finite3(value, "curve second derivative")
}
pub fn bspline_jet3(curve: &BSplineCurve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
Ok(CurveJet {
point: de_boor(
curve,
t,
|p| [p.x, p.y, p.z],
|c| Point3::new(c[0], c[1], c[2]),
)?,
first: de_boor_derivative(
curve,
t,
|p| [p.x, p.y, p.z],
|c| Vec3::new(c[0], c[1], c[2]),
)?,
second: de_boor_second_derivative(
curve,
t,
|p| [p.x, p.y, p.z],
|c| Vec3::new(c[0], c[1], c[2]),
)?,
})
}
pub fn jet3(curve: &Curve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
Ok(CurveJet {
point: evaluate3(curve, t)?,
first: derivative3(curve, t)?,
second: second_derivative3(curve, t)?,
})
}
fn finite(t: Scalar) -> GeomResult<()> {
if t.is_finite() {
Ok(())
} else {
Err(GeomError::InvalidInput(format!(
"curve parameter must be finite, got {t}"
)))
}
}
fn finite2(value: Vec2, what: &str) -> GeomResult<Vec2> {
if value.is_finite() {
Ok(value)
} else {
Err(GeomError::Degenerate(format!("{what} is non-finite")))
}
}
fn finite3(value: Vec3, what: &str) -> GeomResult<Vec3> {
if value.is_finite() {
Ok(value)
} else {
Err(GeomError::Degenerate(format!("{what} is non-finite")))
}
}
fn line_point<P>(origin: P, direction: P, t: Scalar) -> P
where
P: core::ops::Add<Output = P> + core::ops::Mul<Scalar, Output = P>,
{
origin + direction * t
}
fn conic_point2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Point2 {
frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
}
fn conic_tangent2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
}
fn conic_second2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
}
fn conic_point3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Point3 {
frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
}
fn conic_tangent3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
}
fn conic_second3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
}
fn polyline_span(count: usize, closed: bool, t: Scalar) -> Option<(usize, usize, Scalar)> {
let segments = if closed {
count
} else {
count.saturating_sub(1)
};
if count < 2 || segments == 0 {
return None;
}
let clamped = t.clamp(0.0, segments as Scalar);
let mut index = clamped.floor() as usize;
if index >= segments {
index = segments - 1;
}
let local = clamped - index as Scalar;
let next = (index + 1) % count;
Some((index, next, local))
}
fn polyline_point<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
where
P: Copy
+ core::ops::Add<Output = P>
+ core::ops::Sub<Output = P>
+ core::ops::Mul<Scalar, Output = P>,
{
let (i, j, local) = polyline_span(points.len(), closed, t).ok_or_else(|| {
GeomError::Degenerate(format!(
"polyline with {} points has no evaluable segment",
points.len()
))
})?;
Ok(points[i] + (points[j] - points[i]) * local)
}
fn polyline_tangent<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
where
P: Copy + core::ops::Sub<Output = P>,
{
let (i, j, _) = polyline_span(points.len(), closed, t).ok_or_else(|| {
GeomError::Degenerate(format!(
"polyline with {} points has no evaluable segment",
points.len()
))
})?;
Ok(points[j] - points[i])
}
pub(crate) fn span_in(knots: &[Scalar], n: usize, d: usize, u: Scalar) -> usize {
let mut span = d;
for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
if *knot <= u {
span = k;
} else {
break;
}
}
span
}
pub(crate) fn de_boor_recurrence<const N: usize>(
knots: &[Scalar],
span: usize,
d: usize,
u: Scalar,
points: &mut [[Scalar; N]],
weights: &mut [Scalar],
) {
for r in 1..=d {
for j in (r..=d).rev() {
let i = span - d + j;
let denom = knots[i + d + 1 - r] - knots[i];
let alpha = if denom.abs() > 0.0 {
(u - knots[i]) / denom
} else {
0.0
};
for k in 0..N {
points[j][k] = points[j - 1][k] * (1.0 - alpha) + points[j][k] * alpha;
}
weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
}
}
}
fn spline_span<P>(b: &BSplineCurve<P>, t: Scalar) -> GeomResult<(Vec<Scalar>, usize, usize)> {
let axis = SplineAxis::new(
&b.knots,
&b.multiplicities,
b.degree,
b.control_points.len(),
"B-spline curve",
)?;
if let Some(weights) = &b.weights {
if weights.len() != b.control_points.len() {
return Err(GeomError::InvalidInput(format!(
"B-spline has {} weights for {} control points",
weights.len(),
b.control_points.len()
)));
}
if weights
.iter()
.any(|weight| !weight.is_finite() || *weight <= 0.0)
{
return Err(GeomError::InvalidInput(
"B-spline weights must be finite and strictly positive".to_owned(),
));
}
}
let t = axis.clamp(t);
let span = span_in(&axis.knots, axis.count, axis.degree, t);
Ok((axis.knots, span, axis.degree))
}
fn finite_control_points<P, const N: usize, F>(
control_points: &[P],
to: &F,
) -> GeomResult<Vec<[Scalar; N]>>
where
F: Fn(&P) -> [Scalar; N],
{
let points: Vec<[Scalar; N]> = control_points.iter().map(to).collect();
if points
.iter()
.flatten()
.any(|coordinate| !coordinate.is_finite())
{
return Err(GeomError::InvalidInput(
"B-spline control points must be finite".to_owned(),
));
}
Ok(points)
}
fn de_boor<P, const N: usize, F, G, Q>(
b: &BSplineCurve<P>,
t: Scalar,
to: F,
from: G,
) -> GeomResult<Q>
where
F: Fn(&P) -> [Scalar; N],
G: Fn([Scalar; N]) -> Q,
{
let (knots, span, d) = spline_span(b, t)?;
let control_points = finite_control_points(&b.control_points, &to)?;
let u = t.clamp(knots[d], knots[b.control_points.len()]);
let mut work: Vec<[Scalar; N]> = Vec::with_capacity(d + 1);
let mut weights: Vec<Scalar> = Vec::with_capacity(d + 1);
for j in 0..=d {
let idx = span - d + j;
let w = b.weights.as_ref().map_or(1.0, |ws| ws[idx]);
let c = control_points[idx];
let homogeneous = core::array::from_fn(|k| c[k] * w);
if homogeneous.iter().any(|value| !value.is_finite()) {
return Err(GeomError::Degenerate(
"B-spline homogeneous control point overflowed".to_owned(),
));
}
work.push(homogeneous);
weights.push(w);
}
de_boor_recurrence(&knots, span, d, u, &mut work, &mut weights);
let w = weights[d];
if !w.is_finite() || w == 0.0 {
return Err(GeomError::Degenerate(
"B-spline weight collapsed to zero".to_owned(),
));
}
Ok(from(core::array::from_fn(|k| work[d][k] / w)))
}
fn de_boor_derivative<P, const N: usize, F, G, Q>(
b: &BSplineCurve<P>,
t: Scalar,
to: F,
from: G,
) -> GeomResult<Q>
where
F: Fn(&P) -> [Scalar; N],
G: Fn([Scalar; N]) -> Q,
{
let (knots, _, d) = spline_span(b, t)?;
let control_points = finite_control_points(&b.control_points, &to)?;
let n = b.control_points.len();
let u = t.clamp(knots[d], knots[n]);
let hom: Vec<[Scalar; N]> = (0..n)
.map(|i| {
let w = b.weights.as_ref().map_or(1.0, |ws| ws[i]);
let c = control_points[i];
core::array::from_fn(|k| c[k] * w)
})
.collect();
if hom.iter().flatten().any(|value| !value.is_finite()) {
return Err(GeomError::Degenerate(
"B-spline homogeneous control point overflowed".to_owned(),
));
}
let hw: Vec<Scalar> = (0..n)
.map(|i| b.weights.as_ref().map_or(1.0, |ws| ws[i]))
.collect();
let mut dhom: Vec<[Scalar; N]> = Vec::with_capacity(n - 1);
let mut dhw: Vec<Scalar> = Vec::with_capacity(n - 1);
for i in 0..n - 1 {
let denom = knots[i + d + 1] - knots[i + 1];
let f = if denom.abs() > 0.0 {
d as Scalar / denom
} else {
0.0
};
dhom.push(core::array::from_fn(|k| (hom[i + 1][k] - hom[i][k]) * f));
dhw.push((hw[i + 1] - hw[i]) * f);
}
let dknots = &knots[1..knots.len() - 1];
let (da, dw) = eval_homogeneous(dknots, d - 1, &dhom, &dhw, u);
let (a, w) = eval_homogeneous(&knots, d, &hom, &hw, u);
if !w.is_finite() || w == 0.0 {
return Err(GeomError::Degenerate(
"B-spline weight collapsed to zero".to_owned(),
));
}
Ok(from(core::array::from_fn(|k| {
(da[k] - (a[k] / w) * dw) / w
})))
}
fn de_boor_second_derivative<P, const N: usize, F, G, Q>(
b: &BSplineCurve<P>,
t: Scalar,
to: F,
from: G,
) -> GeomResult<Q>
where
F: Fn(&P) -> [Scalar; N],
G: Fn([Scalar; N]) -> Q,
{
let (knots, _, degree) = spline_span(b, t)?;
let control_points = finite_control_points(&b.control_points, &to)?;
let count = b.control_points.len();
let u = t.clamp(knots[degree], knots[count]);
let points: Vec<[Scalar; N]> = (0..count)
.map(|i| {
let weight = b.weights.as_ref().map_or(1.0, |weights| weights[i]);
core::array::from_fn(|axis| control_points[i][axis] * weight)
})
.collect();
if points.iter().flatten().any(|value| !value.is_finite()) {
return Err(GeomError::Degenerate(
"B-spline homogeneous control point overflowed".to_owned(),
));
}
let weights: Vec<Scalar> = (0..count)
.map(|i| b.weights.as_ref().map_or(1.0, |values| values[i]))
.collect();
let (point, weight) = eval_homogeneous(&knots, degree, &points, &weights, u);
if !weight.is_finite() || weight == 0.0 {
return Err(GeomError::Degenerate(
"B-spline weight collapsed to zero".to_owned(),
));
}
let (first_points, first_weights) = derivative_controls(&points, &weights, &knots, degree);
let first_knots = &knots[1..knots.len() - 1];
let (first, first_weight) =
eval_homogeneous(first_knots, degree - 1, &first_points, &first_weights, u);
let position: [Scalar; N] = core::array::from_fn(|axis| point[axis] / weight);
let first_projected: [Scalar; N] =
core::array::from_fn(|axis| (first[axis] - position[axis] * first_weight) / weight);
let (second, second_weight) = if degree == 1 {
([0.0; N], 0.0)
} else {
let (second_points, second_weights) =
derivative_controls(&first_points, &first_weights, first_knots, degree - 1);
let second_knots = &first_knots[1..first_knots.len() - 1];
eval_homogeneous(second_knots, degree - 2, &second_points, &second_weights, u)
};
Ok(from(core::array::from_fn(|axis| {
(second[axis] - 2.0 * first_weight * first_projected[axis] - second_weight * position[axis])
/ weight
})))
}
fn derivative_controls<const N: usize>(
points: &[[Scalar; N]],
weights: &[Scalar],
knots: &[Scalar],
degree: usize,
) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
let mut derivative_points = Vec::with_capacity(points.len() - 1);
let mut derivative_weights = Vec::with_capacity(weights.len() - 1);
for i in 0..points.len() - 1 {
let denominator = knots[i + degree + 1] - knots[i + 1];
let factor = if denominator.abs() > 0.0 {
degree as Scalar / denominator
} else {
0.0
};
derivative_points.push(core::array::from_fn(|axis| {
(points[i + 1][axis] - points[i][axis]) * factor
}));
derivative_weights.push((weights[i + 1] - weights[i]) * factor);
}
(derivative_points, derivative_weights)
}
pub(crate) fn eval_homogeneous<const N: usize>(
knots: &[Scalar],
d: usize,
hom: &[[Scalar; N]],
hw: &[Scalar],
u: Scalar,
) -> ([Scalar; N], Scalar) {
let n = hom.len();
if d == 0 {
let mut idx = 0;
for (k, knot) in knots.iter().enumerate().take(n) {
if *knot <= u {
idx = k;
}
}
return (hom[idx.min(n - 1)], hw[idx.min(n - 1)]);
}
let mut span = d;
for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
if *knot <= u {
span = k;
} else {
break;
}
}
let mut work: Vec<[Scalar; N]> = (0..=d).map(|j| hom[span - d + j]).collect();
let mut weights: Vec<Scalar> = (0..=d).map(|j| hw[span - d + j]).collect();
for r in 1..=d {
for j in (r..=d).rev() {
let i = span - d + j;
let denom = knots[i + d + 1 - r] - knots[i];
let alpha = if denom.abs() > 0.0 {
(u - knots[i]) / denom
} else {
0.0
};
for k in 0..N {
work[j][k] = work[j - 1][k] * (1.0 - alpha) + work[j][k] * alpha;
}
weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
}
}
(work[d], weights[d])
}
pub fn flatten2(
curve: &Curve2,
domain: Interval,
chord_tolerance: Scalar,
max_depth: u32,
) -> GeomResult<Vec<Point2>> {
const MAX_POINTS: usize = 1 << 16;
if !(chord_tolerance.is_finite()
&& chord_tolerance.is_sign_positive()
&& chord_tolerance != 0.0)
{
return Err(GeomError::InvalidInput(format!(
"chord tolerance must be positive and finite, got {chord_tolerance}"
)));
}
if let Curve2::Line(_) = curve {
return Ok(vec![
evaluate2(curve, domain.start)?,
evaluate2(curve, domain.end)?,
]);
}
if let Curve2::Polyline(p) = curve {
let natural = polyline_domain(p.points.len(), p.closed);
let requested = (domain.end - domain.start).abs();
if natural.end > 1.0 && requested <= 1.0 {
return Err(GeomError::InvalidInput(format!(
"polyline domain {:?} spans {requested} of {} segments; a \
polyline parameter is one unit per segment, so this would \
discard {} vertices",
domain,
natural.end,
p.points.len().saturating_sub(2)
)));
}
return polyline_flatten(&p.points, p.closed, domain, |t| evaluate2(curve, t));
}
let mut out = vec![evaluate2(curve, domain.start)?];
let eval = |t| evaluate2(curve, t);
subdivide(
&eval,
domain.start,
domain.end,
chord_tolerance,
max_depth.min(MAX_DEPTH_CEILING),
MAX_POINTS,
&mut out,
)?;
out.push(evaluate2(curve, domain.end)?);
Ok(out)
}
const MAX_DEPTH_CEILING: u32 = 20;
trait ChordPoint: Copy {
fn sub(self, other: Self) -> Self;
fn add_scaled(self, direction: Self, scale: Scalar) -> Self;
fn dot(self, other: Self) -> Scalar;
fn length(self) -> Scalar;
fn length_squared(self) -> Scalar;
}
impl ChordPoint for Point2 {
fn sub(self, other: Self) -> Self {
self - other
}
fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
self + direction * scale
}
fn dot(self, other: Self) -> Scalar {
Point2::dot(self, other)
}
fn length(self) -> Scalar {
Point2::length(self)
}
fn length_squared(self) -> Scalar {
Point2::length_squared(self)
}
}
impl ChordPoint for Point3 {
fn sub(self, other: Self) -> Self {
self - other
}
fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
self + direction * scale
}
fn dot(self, other: Self) -> Scalar {
Point3::dot(self, other)
}
fn length(self) -> Scalar {
Point3::length(self)
}
fn length_squared(self) -> Scalar {
Point3::length_squared(self)
}
}
fn sagitta<P: ChordPoint>(a: P, b: P, m: P) -> Scalar {
let ab = b.sub(a);
let len2 = ab.length_squared();
if len2 <= 0.0 {
return m.sub(a).length();
}
let t = (m.sub(a).dot(ab) / len2).clamp(0.0, 1.0);
m.sub(a.add_scaled(ab, t)).length()
}
fn subdivide<P, F>(
eval: &F,
a: Scalar,
b: Scalar,
tol: Scalar,
depth: u32,
budget: usize,
out: &mut Vec<P>,
) -> GeomResult<()>
where
P: ChordPoint,
F: Fn(Scalar) -> GeomResult<P>,
{
if out.len() >= budget {
return Err(GeomError::Degenerate(format!(
"curve flattening exceeded {budget} points before meeting the \
chord tolerance {tol}; the curve may be degenerate"
)));
}
let mid = 0.5 * (a + b);
let pa = eval(a)?;
let pb = eval(b)?;
if !(mid > a && mid < b) {
if sagitta(pa, pb, pa) <= tol && (pb.sub(pa)).length() <= tol {
return Ok(());
}
return Err(GeomError::Degenerate(format!(
"curve parameter interval ({a}, {b}) is too small to bisect but \
its chord still exceeds the tolerance {tol}"
)));
}
let pm = eval(mid)?;
if sagitta(pa, pb, pm) <= tol {
return Ok(());
}
if depth == 0 {
return Err(GeomError::BudgetExceeded {
resource: "curve flattening depth",
});
}
subdivide(eval, a, mid, tol, depth - 1, budget, out)?;
out.push(pm);
subdivide(eval, mid, b, tol, depth - 1, budget, out)?;
Ok(())
}
fn polyline_flatten<P, F>(
points: &[P],
closed: bool,
domain: Interval,
eval: F,
) -> GeomResult<Vec<P>>
where
P: Copy,
F: Fn(Scalar) -> GeomResult<P>,
{
let segments = if closed {
points.len()
} else {
points.len().saturating_sub(1)
};
if segments == 0 {
return Err(GeomError::Degenerate(
"polyline has no evaluable segment".to_owned(),
));
}
let lo = domain.start.min(domain.end);
let hi = domain.start.max(domain.end);
let mut out = vec![eval(lo)?];
let first = lo.floor() as i64 + 1;
let last = hi.ceil() as i64 - 1;
for k in first..=last {
let t = k as Scalar;
if t > lo && t < hi {
out.push(eval(t)?);
}
}
out.push(eval(hi)?);
Ok(out)
}
impl CurveEvaluator<Curve2> for ScalarCurve {
type Point = Point2;
type Derivative = Vec2;
type Error = GeomError;
fn domain(&self, curve: &Curve2) -> Interval {
domain2(curve)
}
fn evaluate(
&self,
curve: &Curve2,
t: Scalar,
_tolerance: Tolerance,
) -> Result<Self::Point, Self::Error> {
evaluate2(curve, t)
}
fn derivative(
&self,
curve: &Curve2,
t: Scalar,
_tolerance: Tolerance,
) -> Result<Self::Derivative, Self::Error> {
derivative2(curve, t)
}
}
impl CurveEvaluator<Curve3> for ScalarCurve {
type Point = Point3;
type Derivative = Vec3;
type Error = GeomError;
fn domain(&self, curve: &Curve3) -> Interval {
domain3(curve)
}
fn evaluate(
&self,
curve: &Curve3,
t: Scalar,
_tolerance: Tolerance,
) -> Result<Self::Point, Self::Error> {
evaluate3(curve, t)
}
fn derivative(
&self,
curve: &Curve3,
t: Scalar,
_tolerance: Tolerance,
) -> Result<Self::Derivative, Self::Error> {
derivative3(curve, t)
}
}
#[allow(unused)]
fn _type_anchors(_: Circle2, _: Circle3, _: Ellipse2, _: Ellipse3, _: Line2, _: Line3) {}
#[allow(unused)]
fn _poly_anchors(_: Polyline2, _: Polyline3) {}
pub fn flatten3(
curve: &Curve3,
domain: Interval,
chord_tolerance: Scalar,
max_depth: u32,
) -> GeomResult<Vec<Point3>> {
const MAX_POINTS: usize = 1 << 16;
if !(chord_tolerance.is_finite()
&& chord_tolerance.is_sign_positive()
&& chord_tolerance != 0.0)
{
return Err(GeomError::InvalidInput(format!(
"chord tolerance must be positive and finite, got {chord_tolerance}"
)));
}
if let Curve3::Line(_) = curve {
return Ok(vec![
evaluate3(curve, domain.start)?,
evaluate3(curve, domain.end)?,
]);
}
if let Curve3::Polyline(p) = curve {
let natural = polyline_domain(p.points.len(), p.closed);
let requested = (domain.end - domain.start).abs();
if natural.end > 1.0 && requested <= 1.0 {
return Err(GeomError::InvalidInput(format!(
"polyline domain {:?} spans {requested} of {} segments; a \
polyline parameter is one unit per segment, so this would \
discard {} vertices",
domain,
natural.end,
p.points.len().saturating_sub(2)
)));
}
return polyline_flatten(&p.points, p.closed, domain, |t| evaluate3(curve, t));
}
let eval = |t| evaluate3(curve, t);
let mut out = vec![eval(domain.start)?];
subdivide(
&eval,
domain.start,
domain.end,
chord_tolerance,
max_depth.min(MAX_DEPTH_CEILING),
MAX_POINTS,
&mut out,
)?;
out.push(eval(domain.end)?);
Ok(out)
}
fn no_closed_form_inversion() -> GeomError {
GeomError::InvalidInput(
"curve family has no closed form inversion; a point trim on this basis \
would require iteration, which trim resolution does not perform"
.to_owned(),
)
}
fn point_not_on_curve(distance: Scalar, tolerance: Scalar) -> GeomError {
GeomError::InvalidInput(format!(
"point is {distance} from the curve, outside the {tolerance} tolerance; \
refusing rather than projecting it onto the nearest parameter"
))
}
fn invert_line(
origin: impl Into<[Scalar; 3]>,
direction: [Scalar; 3],
point: [Scalar; 3],
tolerance: Scalar,
) -> GeomResult<Scalar> {
let origin = origin.into();
let dd = direction.iter().map(|c| c * c).sum::<Scalar>();
if !dd.is_finite() || dd <= Scalar::EPSILON {
return Err(GeomError::InvalidInput(
"line direction is degenerate, so no parameter names a point".to_owned(),
));
}
let offset = [
point[0] - origin[0],
point[1] - origin[1],
point[2] - origin[2],
];
let t = offset
.iter()
.zip(direction.iter())
.map(|(o, d)| o * d)
.sum::<Scalar>()
/ dd;
let residual = [
offset[0] - direction[0] * t,
offset[1] - direction[1] * t,
offset[2] - direction[2] * t,
];
let distance = residual.iter().map(|c| c * c).sum::<Scalar>().sqrt();
if distance > tolerance {
return Err(point_not_on_curve(distance, tolerance));
}
Ok(t)
}
fn invert_conic(
local_x: Scalar,
local_y: Scalar,
semi_x: Scalar,
semi_y: Scalar,
) -> GeomResult<Scalar> {
if !(semi_x.is_finite() && semi_y.is_finite()) || semi_x <= 0.0 || semi_y <= 0.0 {
return Err(GeomError::InvalidInput(
"conic semi-axes must be finite and positive to invert a point".to_owned(),
));
}
let angle = (local_y / semi_y).atan2(local_x / semi_x);
if !angle.is_finite() {
return Err(GeomError::InvalidInput(
"conic inversion produced a non-finite angle".to_owned(),
));
}
Ok(angle.rem_euclid(std::f64::consts::TAU))
}
pub fn invert2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
finite2(point, "inversion point")?;
let linear = tolerance.linear();
match curve {
Curve2::Line(l) => invert_line(
[l.origin.x, l.origin.y, 0.0],
[l.direction.x, l.direction.y, 0.0],
[point.x, point.y, 0.0],
linear,
),
Curve2::Circle(c) => {
let t = invert_conic_in_frame2(&c.frame, point, c.radius, c.radius)?;
verify2(curve, t, point, linear)
}
Curve2::Ellipse(e) => {
let t = invert_conic_in_frame2(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
verify2(curve, t, point, linear)
}
Curve2::Sinusoid(_) | Curve2::QuadraticGraph(_) => verify2(curve, point.x, point, linear),
Curve2::AngleGraph(g) => {
let u = g.angle(point.y).ok_or_else(|| outside_graph(point.y))?;
let turns = ((point.x - u) / std::f64::consts::TAU).round();
let shifted = Point2::new(point.x - turns * std::f64::consts::TAU, point.y);
verify2(curve, point.y, shifted, linear)
}
Curve2::Lifted(l) => {
let lifted = l.carrier.jet(point.x, point.y).point;
let t = invert3(&l.curve, lifted, tolerance)?;
verify2(curve, t, point, linear)
}
Curve2::Implicit(c) => {
let t = c
.parameter_of(point)
.ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
verify2(curve, t, point, linear)
}
_ => Err(no_closed_form_inversion()),
}
}
fn invert_conic_in_frame2(
frame: &axiolid_core::Frame2,
point: Point2,
semi_x: Scalar,
semi_y: Scalar,
) -> GeomResult<Scalar> {
let offset = point - frame.origin;
invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
}
fn verify2(curve: &Curve2, t: Scalar, point: Point2, tolerance: Scalar) -> GeomResult<Scalar> {
let found = evaluate2(curve, t)?;
let distance = (found - point).length();
if distance > tolerance {
return Err(point_not_on_curve(distance, tolerance));
}
Ok(t)
}
pub fn invert3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
finite3(point, "inversion point")?;
let linear = tolerance.linear();
match curve {
Curve3::Line(l) => invert_line(
[l.origin.x, l.origin.y, l.origin.z],
[l.direction.x, l.direction.y, l.direction.z],
[point.x, point.y, point.z],
linear,
),
Curve3::Circle(c) => {
let t = invert_conic_in_frame3(&c.frame, point, c.radius, c.radius)?;
verify3(curve, t, point, linear)
}
Curve3::Ellipse(e) => {
let t = invert_conic_in_frame3(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
verify3(curve, t, point, linear)
}
Curve3::RuledSection(r) => {
let carrier = axiolid_curve::Carrier::Ruled(r.carrier);
let (u, _) = carrier.parameters(point);
first_on_curve(curve, &turns_of(u), point, linear)
}
Curve3::TorusSection(r) => {
let carrier = axiolid_curve::Carrier::Torus(r.torus);
let (_, v) = carrier.parameters(point);
first_on_curve(curve, &turns_of(v), point, linear)
}
Curve3::PairSection(r) => {
let t = r
.parameter_of(point)
.ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
verify3(curve, t, point, linear)
}
Curve3::ImplicitSection(r) => {
if let axiolid_curve::Carrier::Spline(b) = &r.carrier {
let surface = axiolid_surface::Surface::BSpline((**b).clone());
let (u, v) = crate::surface::locate(&surface, point, tolerance)?;
let t = r
.curve
.parameter_of(Point2::new(u, v))
.ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
return verify3(curve, t, point, linear);
}
let (u, v) = r.carrier.parameters(point);
let (pu, pv) = r.carrier.periodic();
let turns = |periodic: bool| -> &'static [Scalar] {
if periodic {
&[0.0, 1.0, -1.0, 2.0, -2.0]
} else {
&[0.0]
}
};
let mut candidates = Vec::new();
for &ku in turns(pu) {
for &kv in turns(pv) {
let tau = std::f64::consts::TAU;
if let Some(t) = r
.curve
.parameter_of(Point2::new(u + ku * tau, v + kv * tau))
{
candidates.push(t);
}
}
}
first_on_curve(curve, &candidates, point, linear)
}
_ => Err(no_closed_form_inversion()),
}
}
fn turns_of(angle: Scalar) -> Vec<Scalar> {
let tau = std::f64::consts::TAU;
vec![
angle,
angle + tau,
angle - tau,
angle + 2.0 * tau,
angle - 2.0 * tau,
]
}
fn first_on_curve(
curve: &Curve3,
candidates: &[Scalar],
point: Point3,
tolerance: Scalar,
) -> GeomResult<Scalar> {
let mut nearest = Scalar::INFINITY;
for &t in candidates {
if let Ok(found) = evaluate3(curve, t) {
let distance = (found - point).length();
if distance <= tolerance {
return Ok(t);
}
nearest = nearest.min(distance);
}
}
Err(point_not_on_curve(nearest, tolerance))
}
pub fn locate3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
match invert3(curve, point, tolerance) {
Ok(t) => Ok(t),
Err(error) => match curve {
Curve3::BSpline(b) => {
let domain = spline_domain(b);
let t = nearest_parameter(
domain,
b.control_points.len().max(2) * 64,
|t| evaluate3(curve, t).map(|p| (p - point).length()),
|t| {
let (p, d, dd) = (
evaluate3(curve, t)?,
derivative3(curve, t)?,
second_derivative3(curve, t)?,
);
let r = p - point;
Ok((r.dot(d), d.dot(d) + r.dot(dd)))
},
)?;
verify3(curve, t, point, tolerance.linear())
}
_ => Err(error),
},
}
}
pub fn locate2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
match invert2(curve, point, tolerance) {
Ok(t) => Ok(t),
Err(error) => match curve {
Curve2::BSpline(b) => {
let domain = spline_domain(b);
let t = nearest_parameter(
domain,
b.control_points.len().max(2) * 64,
|t| evaluate2(curve, t).map(|p| (p - point).length()),
|t| {
let (p, d, dd) = (
evaluate2(curve, t)?,
derivative2(curve, t)?,
second_derivative2(curve, t)?,
);
let r = p - point;
Ok((r.dot(d), d.dot(d) + r.dot(dd)))
},
)?;
verify2(curve, t, point, tolerance.linear())
}
_ => Err(error),
},
}
}
fn nearest_parameter(
domain: Interval,
samples: usize,
distance: impl Fn(Scalar) -> GeomResult<Scalar>,
gradient: impl Fn(Scalar) -> GeomResult<(Scalar, Scalar)>,
) -> GeomResult<Scalar> {
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
let mut best = (Scalar::INFINITY, lo);
for i in 0..=samples {
let t = lo + (hi - lo) * i as Scalar / samples as Scalar;
let d = distance(t)?;
if d < best.0 {
best = (d, t);
}
}
let mut t = best.1;
for _ in 0..60 {
let (g, h) = gradient(t)?;
if h <= 0.0 || !h.is_finite() {
break;
}
let next = (t - g / h).clamp(lo, hi);
let moved = (next - t).abs();
t = next;
if moved <= 4.0 * Scalar::EPSILON * (1.0 + t.abs()) {
break;
}
}
Ok(t)
}
fn invert_conic_in_frame3(
frame: &axiolid_core::Frame3,
point: Point3,
semi_x: Scalar,
semi_y: Scalar,
) -> GeomResult<Scalar> {
let offset = point - frame.origin;
invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
}
fn verify3(curve: &Curve3, t: Scalar, point: Point3, tolerance: Scalar) -> GeomResult<Scalar> {
let found = evaluate3(curve, t)?;
let distance = (found - point).length();
if distance > tolerance {
return Err(point_not_on_curve(distance, tolerance));
}
Ok(t)
}