use std::sync::OnceLock;
use super::analytic::in_period;
use crate::curves::Side;
use crate::{Continuity, Point, Tolerance, Vector};
pub(crate) const MAX_DEGREE: usize = 25;
pub(crate) use crate::curves::BSplineConstructionError;
pub(crate) fn flat_knots(knots: &[f64], mults: &[u32]) -> Vec<f64> {
let mut out = Vec::with_capacity(mults.iter().map(|&m| m as usize).sum());
for (&k, &m) in knots.iter().zip(mults.iter()) {
out.extend(std::iter::repeat_n(k, m as usize));
}
out
}
pub(crate) fn distinct_knots(flat: &[f64]) -> (Vec<f64>, Vec<u32>) {
flat.chunk_by(|a, b| a == b)
.map(|run| (run[0], run.len() as u32))
.unzip()
}
#[derive(Debug, Clone)]
pub(crate) struct KnotVector {
degree: usize,
periodic: bool,
flat: Vec<f64>,
distinct: OnceLock<Box<(Vec<f64>, Vec<u32>)>>,
pole_count: usize,
seam_multiplicity: usize,
}
impl PartialEq for KnotVector {
fn eq(&self, other: &KnotVector) -> bool {
(self.degree, self.periodic) == (other.degree, other.periodic) && self.flat == other.flat
}
}
impl KnotVector {
pub(crate) fn new(
degree: usize,
knots: Vec<f64>,
multiplicities: Vec<u32>,
periodic: bool,
pole_count: usize,
) -> Result<KnotVector, BSplineConstructionError> {
validate_direction(degree, pole_count, &knots, &multiplicities, periodic)?;
let flat = flat_knots(&knots, &multiplicities);
Ok(KnotVector::from_validated(
degree,
knots,
multiplicities,
flat,
periodic,
))
}
pub(crate) fn from_validated(
degree: usize,
knots: Vec<f64>,
multiplicities: Vec<u32>,
flat: Vec<f64>,
periodic: bool,
) -> KnotVector {
debug_assert_eq!(flat, flat_knots(&knots, &multiplicities));
let last = multiplicities[multiplicities.len() - 1] as usize;
let pole_count = if periodic {
flat.len() - last
} else {
flat.len() - degree - 1
};
debug_assert!(
validate_direction(degree, pole_count, &knots, &multiplicities, periodic).is_ok()
);
KnotVector {
degree,
periodic,
seam_multiplicity: multiplicities[0] as usize,
flat,
distinct: OnceLock::from(Box::new((knots, multiplicities))),
pole_count,
}
}
pub(crate) fn from_flat(degree: usize, flat: Vec<f64>, periodic: bool) -> KnotVector {
let (first, last) = (flat[0], flat[flat.len() - 1]);
let seam_multiplicity = flat.iter().take_while(|&&k| k == first).count();
let last = flat.iter().rev().take_while(|&&k| k == last).count();
let pole_count = if periodic {
flat.len() - last
} else {
flat.len() - degree - 1
};
let direction = KnotVector {
degree,
periodic,
seam_multiplicity,
flat,
distinct: OnceLock::new(),
pole_count,
};
debug_assert!(
validate_direction(
degree,
pole_count,
direction.knots(),
direction.multiplicities(),
periodic
)
.is_ok()
);
direction
}
pub(crate) fn bezier(degree: usize, first: f64, last: f64) -> KnotVector {
let mut flat = vec![first; 2 * degree + 2];
flat[degree + 1..].fill(last);
KnotVector::from_flat(degree, flat, false)
}
pub(crate) fn degree(&self) -> usize {
self.degree
}
pub(crate) fn is_periodic(&self) -> bool {
self.periodic
}
pub(crate) fn knots(&self) -> &[f64] {
&self.distinct().0
}
pub(crate) fn multiplicities(&self) -> &[u32] {
&self.distinct().1
}
fn distinct(&self) -> &(Vec<f64>, Vec<u32>) {
self.distinct
.get_or_init(|| Box::new(distinct_knots(&self.flat)))
}
pub(crate) fn flat(&self) -> &[f64] {
&self.flat
}
pub(crate) fn pole_count(&self) -> usize {
self.pole_count
}
pub(crate) fn bounds(&self) -> (f64, f64) {
let flat = &self.flat;
if self.periodic {
(flat[0], flat[flat.len() - 1])
} else {
(flat[self.degree], flat[flat.len() - 1 - self.degree])
}
}
pub(crate) fn period(&self) -> Option<f64> {
let (first, last) = self.bounds();
self.periodic.then_some(last - first)
}
#[inline]
pub(crate) fn span_knots<const N: usize>(
&self,
u: f64,
left: &mut [f64; N],
right: &mut [f64; N],
) -> usize {
let degree = self.degree;
debug_assert!(degree < N, "basis table too narrow for degree {degree}");
if self.periodic {
return self.periodic_span_knots(u, left, right);
}
let (span, u) = self.span(u);
let flat = &self.flat;
for j in 1..=degree {
left[j] = u - flat[span + 1 - j];
right[j] = flat[span + j] - u;
}
span - degree
}
#[inline(always)]
fn span(&self, u: f64) -> (usize, f64) {
let flat = &self.flat;
let lo = self.degree;
let hi = flat.len() - 1 - self.degree;
let u = u.clamp(flat[lo], flat[hi]);
let mut span = lo;
let mut length = hi - lo;
while length > 1 {
let half = length / 2;
if flat[span + half] <= u {
span += half;
}
length -= half;
}
while span > lo && flat[span] >= flat[span + 1] {
span -= 1;
}
(span, u)
}
fn span_below(&self, u: f64) -> (usize, f64) {
let (s, u) = self.span(u);
let flat = &self.flat;
let lo = self.degree;
if u != flat[s] || s == lo {
return (s, u);
}
let mut t = s - 1;
while t > lo && flat[t] >= flat[t + 1] {
t -= 1;
}
if flat[t] < u { (t, u) } else { (s, u) }
}
pub(crate) fn span_knots_sided<const N: usize>(
&self,
u: f64,
side: Side,
left: &mut [f64; N],
right: &mut [f64; N],
) -> usize {
if self.periodic {
return self.periodic_span_knots_sided(u, side, left, right);
}
if side == Side::Above {
return self.span_knots(u, left, right);
}
let degree = self.degree;
debug_assert!(degree < N, "basis table too narrow for degree {degree}");
let (span, u) = self.span_below(u);
let flat = &self.flat;
for j in 1..=degree {
left[j] = u - flat[span + 1 - j];
right[j] = flat[span + j] - u;
}
span - degree
}
#[inline(always)]
pub(crate) fn low_degree_basis(&self, u: f64) -> Option<(usize, [f64; 4])> {
if self.periodic {
return None;
}
match self.degree {
1 => Some(self.basis_of_degree::<1>(u)),
2 => Some(self.basis_of_degree::<2>(u)),
3 => Some(self.basis_of_degree::<3>(u)),
_ => None,
}
}
#[inline(always)]
fn basis_of_degree<const P: usize>(&self, u: f64) -> (usize, [f64; 4]) {
let (span, u) = self.span(u);
let around = &self.flat[span + 1 - P..span + 1 + P];
let left: [f64; 4] = std::array::from_fn(|j| {
if (1..=P).contains(&j) {
u - around[P - j]
} else {
0.0
}
});
let right: [f64; 4] = std::array::from_fn(|j| {
if (1..=P).contains(&j) {
around[P - 1 + j] - u
} else {
0.0
}
});
(span - P, basis_values_of::<P>(&left, &right))
}
pub(crate) fn span_first_pole(&self, u: f64) -> usize {
let (mut left, mut right) = ([0.0; LARGE], [0.0; LARGE]);
self.span_knots(u, &mut left, &mut right)
}
fn periodic_span_knots<const N: usize>(
&self,
u: f64,
left: &mut [f64; N],
right: &mut [f64; N],
) -> usize {
let (first, last) = self.bounds();
let u = if u == last {
last
} else {
in_period(u, first, last)
};
let block = &self.flat[..self.pole_count];
let local = block
.partition_point(|&k| k <= u)
.saturating_sub(1)
.min(block.len() - 1);
self.periodic_span_at(u, local, left, right)
}
fn periodic_span_knots_sided<const N: usize>(
&self,
u: f64,
side: Side,
left: &mut [f64; N],
right: &mut [f64; N],
) -> usize {
let (first, last) = self.bounds();
let mut u = in_period(u, first, last);
if u >= last {
u = first;
}
let block = &self.flat[..self.pole_count];
let local = match side {
Side::Above => block.partition_point(|&k| k <= u),
Side::Below if u == first => {
u = last;
block.len()
}
Side::Below => block.partition_point(|&k| k < u),
}
.saturating_sub(1)
.min(block.len() - 1);
self.periodic_span_at(u, local, left, right)
}
#[inline(always)]
fn periodic_span_at<const N: usize>(
&self,
u: f64,
local: usize,
left: &mut [f64; N],
right: &mut [f64; N],
) -> usize {
let (first, last) = self.bounds();
let period = last - first;
let block = &self.flat[..self.pole_count];
let len = block.len() as isize;
let knot = |offset: isize| {
let k = local as isize + offset;
block[k.rem_euclid(len) as usize] + period * k.div_euclid(len) as f64
};
for j in 1..=self.degree {
left[j] = u - knot(1 - j as isize);
right[j] = knot(j as isize) - u;
}
(local + self.pole_count + 1 - self.seam_multiplicity) % self.pole_count
}
pub(crate) fn window<const N: usize>(&self, u: f64, order: usize, w: &mut Window<N>) {
let mut left = [0.0f64; N];
let mut right = [0.0f64; N];
w.pole_count = self.pole_count;
w.periodic = self.periodic;
w.first = self.span_knots(u, &mut left, &mut right);
match order {
0 => basis_values(self.degree, &left, &right, &mut w.ders[0]),
1 => basis_first(self.degree, &left, &right, &mut w.ders),
_ => basis_second(self.degree, &left, &right, &mut w.ders),
}
}
#[inline]
pub(crate) fn window_sided<const N: usize>(
&self,
u: f64,
order: usize,
side: Option<Side>,
w: &mut Window<N>,
) {
let side = match side {
None => return self.window(u, order, w),
Some(Side::Above) if !self.periodic => return self.window(u, order, w),
Some(side) => side,
};
let mut left = [0.0f64; N];
let mut right = [0.0f64; N];
w.pole_count = self.pole_count;
w.periodic = self.periodic;
w.first = self.span_knots_sided(u, side, &mut left, &mut right);
match order {
0 => basis_values(self.degree, &left, &right, &mut w.ders[0]),
1 => basis_first(self.degree, &left, &right, &mut w.ders),
_ => basis_second(self.degree, &left, &right, &mut w.ders),
}
}
#[inline]
pub(crate) fn basis<const N: usize>(&self, u: f64, values: &mut [f64; N]) -> usize {
let mut left = [0.0f64; N];
let mut right = [0.0f64; N];
let first = self.span_knots(u, &mut left, &mut right);
basis_values(self.degree, &left, &right, values);
first
}
#[inline]
pub(crate) fn pole_index(&self, first: usize, j: usize) -> usize {
wrap(first + j, self.pole_count, self.periodic)
}
#[inline]
pub(crate) fn wraps(&self, first: usize) -> bool {
first + self.degree >= self.pole_count
}
pub(crate) fn breaks(&self) -> Vec<f64> {
let (lo, hi) = self.bounds();
let mut breaks = vec![lo];
breaks.extend(self.knots().iter().copied().filter(|&k| k > lo && k < hi));
breaks.push(hi);
breaks
}
pub(crate) fn crease_breaks(&self, breaks: &[f64]) -> Vec<bool> {
let last = breaks.len() - 1;
let creased = |m: u32| m as usize >= self.degree;
breaks
.iter()
.enumerate()
.map(|(i, &x)| {
if i == 0 || i == last {
!self.periodic || creased(self.multiplicities()[0])
} else {
self.knots()
.iter()
.position(|&k| k == x)
.is_some_and(|k| creased(self.multiplicities()[k]))
}
})
.collect()
}
pub(crate) fn smooth_regions(&self) -> Option<Vec<(f64, f64)>> {
let (lo, hi) = self.bounds();
let creases: Vec<f64> = self
.knots()
.iter()
.zip(self.multiplicities())
.filter(|&(&k, &m)| k > lo && k < hi && m as usize >= self.degree)
.map(|(&k, _)| k)
.collect();
let smooth_seam = self.periodic && self.seam_multiplicity < self.degree;
if smooth_seam {
if creases.is_empty() {
return None;
}
let mut regions: Vec<(f64, f64)> = creases.windows(2).map(|w| (w[0], w[1])).collect();
regions.push((creases[creases.len() - 1], creases[0] + (hi - lo)));
return Some(regions);
}
let mut cuts = vec![lo];
cuts.extend(creases);
cuts.push(hi);
Some(cuts.windows(2).map(|w| (w[0], w[1])).collect())
}
pub(crate) fn span_bezier_matrices(&self, breaks: &[f64]) -> (Vec<usize>, Vec<f64>) {
fn fill<const N: usize>(knots: &KnotVector, breaks: &[f64]) -> (Vec<usize>, Vec<f64>) {
let n = knots.degree + 1;
let mut firsts = Vec::with_capacity(breaks.len() - 1);
let mut matrices = Vec::with_capacity((breaks.len() - 1) * n * n);
let mut matrix = [[0.0f64; N]; N];
for w in breaks.windows(2) {
firsts.push(knots.bezier_matrix(0.5 * (w[0] + w[1]), &mut matrix));
for row in &matrix[..n] {
matrices.extend_from_slice(&row[..n]);
}
}
(firsts, matrices)
}
if self.degree < SMALL {
fill::<SMALL>(self, breaks)
} else {
fill::<LARGE>(self, breaks)
}
}
#[allow(clippy::needless_range_loop)] pub(crate) fn bezier_matrix<const N: usize>(
&self,
u: f64,
matrix: &mut [[f64; N]; N],
) -> usize {
let p = self.degree;
let mut left = [0.0f64; N];
let mut right = [0.0f64; N];
let first = self.span_knots(u, &mut left, &mut right);
let knot = |m: usize| {
if m < p {
-left[p - m]
} else {
right[m + 1 - p]
}
};
let (a, b) = (knot(p - 1), knot(p));
let mut inverse = [[0.0f64; N]; N];
for r in 1..=p {
for j in r..=p {
inverse[r][j] = 1.0 / (knot(p + j - r) - knot(j - 1));
}
}
let mut d = [[0.0f64; N]; N];
for k in 0..=p {
for (j, dj) in d.iter_mut().enumerate().take(p + 1) {
dj[..=p].fill(0.0);
dj[j] = 1.0;
}
for r in 1..=p {
let x = if r <= k { b } else { a };
for j in (r..=p).rev() {
let alpha = (x - knot(j - 1)) * inverse[r][j];
for i in j - r..=j {
d[j][i] = (1.0 - alpha) * d[j - 1][i] + alpha * d[j][i];
}
}
}
matrix[k][..=p].copy_from_slice(&d[p][..=p]);
}
first
}
}
#[derive(Clone, Copy)]
pub(crate) struct Poles<'a> {
pub(crate) points: &'a [Point],
pub(crate) weights: Option<&'a [f64]>,
}
impl Poles<'_> {
#[inline]
pub(crate) fn homogeneous(&self, i: usize) -> [f64; 4] {
let p = self.points[i];
match self.weights {
Some(weights) => {
let w = weights[i];
[p.x * w, p.y * w, p.z * w, w]
}
None => [p.x, p.y, p.z, 1.0],
}
}
}
pub(crate) const MAX_DERIVATIVE: usize = 2;
pub(crate) const SMALL: usize = 8;
pub(crate) const LARGE: usize = MAX_DEGREE + 1;
pub(crate) struct Window<const N: usize> {
pub(crate) ders: [[f64; N]; MAX_DERIVATIVE + 1],
first: usize,
pole_count: usize,
periodic: bool,
}
impl<const N: usize> Window<N> {
pub(crate) fn new() -> Self {
Window {
ders: [[0.0; N]; MAX_DERIVATIVE + 1],
first: 0,
pole_count: 0,
periodic: false,
}
}
#[inline]
pub(crate) fn pole(&self, j: usize) -> usize {
wrap(self.first + j, self.pole_count, self.periodic)
}
}
#[inline]
fn wrap(index: usize, pole_count: usize, periodic: bool) -> usize {
if periodic && index >= pole_count {
index % pole_count
} else {
index
}
}
pub(crate) fn basis_at<const N: usize>(
u: f64,
degree: usize,
span: usize,
flat: &[f64],
left: &mut [f64; N],
right: &mut [f64; N],
values: &mut [f64; N],
) {
for j in 1..=degree {
left[j] = u - flat[span + 1 - j];
right[j] = flat[span + j] - u;
}
basis_values(degree, left, right, values);
}
fn basis_values<const N: usize>(
degree: usize,
left: &[f64; N],
right: &[f64; N],
out: &mut [f64; N],
) {
out[0] = 1.0;
for j in 1..=degree {
let mut saved = 0.0;
for r in 0..j {
let temp = out[r] * (1.0 / (right[r + 1] + left[j - r]));
out[r] = saved + right[r + 1] * temp;
saved = left[j - r] * temp;
}
out[j] = saved;
}
}
fn basis_first<const N: usize>(
degree: usize,
left: &[f64; N],
right: &[f64; N],
ders: &mut [[f64; N]; MAX_DERIVATIVE + 1],
) {
let p = degree;
let [values, firsts, _] = ders;
values[0] = 1.0;
for j in 1..p {
let mut saved = 0.0;
for r in 0..j {
let temp = values[r] * (1.0 / (right[r + 1] + left[j - r]));
values[r] = saved + right[r + 1] * temp;
saved = left[j - r] * temp;
}
values[j] = saved;
}
let mut saved = 0.0;
let mut previous = 0.0;
for r in 0..p {
let temp = values[r] * (1.0 / (right[r + 1] + left[p - r]));
values[r] = saved + right[r + 1] * temp;
saved = left[p - r] * temp;
firsts[r] = p as f64 * (previous - temp);
previous = temp;
}
values[p] = saved;
firsts[p] = p as f64 * previous;
}
fn basis_second<const N: usize>(
degree: usize,
left: &[f64; N],
right: &[f64; N],
ders: &mut [[f64; N]; MAX_DERIVATIVE + 1],
) {
let p = degree;
let [values, firsts, seconds] = ders;
let mut lower = [0.0f64; N];
values[0] = 1.0;
for j in 1..p.saturating_sub(1) {
let mut saved = 0.0;
for r in 0..j {
let temp = values[r] * (1.0 / (right[r + 1] + left[j - r]));
values[r] = saved + right[r + 1] * temp;
saved = left[j - r] * temp;
}
values[j] = saved;
}
if p >= 2 {
let q = p - 1;
let mut saved = 0.0;
let mut previous = 0.0;
for r in 0..q {
let temp = values[r] * (1.0 / (right[r + 1] + left[q - r]));
values[r] = saved + right[r + 1] * temp;
saved = left[q - r] * temp;
lower[r] = q as f64 * (previous - temp);
previous = temp;
}
values[q] = saved;
lower[q] = q as f64 * previous;
}
let mut saved = 0.0;
let mut previous = 0.0;
let mut previous_lower = 0.0;
for r in 0..p {
let inverse = 1.0 / (right[r + 1] + left[p - r]);
let temp = values[r] * inverse;
values[r] = saved + right[r + 1] * temp;
saved = left[p - r] * temp;
firsts[r] = p as f64 * (previous - temp);
previous = temp;
let lower_temp = lower[r] * inverse;
seconds[r] = p as f64 * (previous_lower - lower_temp);
previous_lower = lower_temp;
}
values[p] = saved;
firsts[p] = p as f64 * previous;
seconds[p] = p as f64 * previous_lower;
}
#[cfg(test)]
fn basis_derivatives<const N: usize>(
degree: usize,
left: &[f64; N],
right: &[f64; N],
order: usize,
ders: &mut [[f64; N]; MAX_DERIVATIVE + 1],
) {
let p = degree;
let mut ndu = [[0.0f64; N]; N];
ndu[0][0] = 1.0;
for j in 1..=p {
let mut saved = 0.0;
for r in 0..j {
ndu[j][r] = right[r + 1] + left[j - r];
let temp = ndu[r][j - 1] / ndu[j][r];
ndu[r][j] = saved + right[r + 1] * temp;
saved = left[j - r] * temp;
}
ndu[j][j] = saved;
}
for j in 0..=p {
ders[0][j] = ndu[j][p];
}
let order = order.min(p).min(MAX_DERIVATIVE);
if order == 0 {
return;
}
let mut a = [[0.0f64; MAX_DERIVATIVE + 1]; 2];
for r in 0..=p {
let (mut s1, mut s2) = (0, 1);
a[0][0] = 1.0;
for k in 1..=order {
let mut d = 0.0;
let pk = p - k;
if r >= k {
let rk = r - k;
a[s2][0] = a[s1][0] / ndu[pk + 1][rk];
d = a[s2][0] * ndu[rk][pk];
}
let j1 = if r + 1 >= k { 1 } else { k - r };
let j2 = if r <= pk + 1 { k - 1 } else { p - r };
for j in j1..=j2 {
let rkj = r + j - k;
a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rkj];
d += a[s2][j] * ndu[rkj][pk];
}
if r <= pk {
a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
d += a[s2][k] * ndu[r][pk];
}
ders[k][r] = d;
std::mem::swap(&mut s1, &mut s2);
}
}
let mut factor = p as f64;
for (k, row) in ders.iter_mut().enumerate().take(order + 1).skip(1) {
for value in row.iter_mut().take(p + 1) {
*value *= factor;
}
factor *= (p - k) as f64;
}
}
#[inline]
pub(crate) fn point(knots: &KnotVector, poles: Poles, u: f64) -> Point {
if !knots.periodic {
match knots.degree {
1 => return point_of_degree::<1>(knots, poles, u),
2 => return point_of_degree::<2>(knots, poles, u),
3 => return point_of_degree::<3>(knots, poles, u),
_ => {}
}
}
if knots.degree < SMALL {
point_sized::<SMALL>(knots, poles, u)
} else {
point_sized::<LARGE>(knots, poles, u)
}
}
#[inline(always)]
fn point_of_degree<const P: usize>(knots: &KnotVector, poles: Poles, u: f64) -> Point {
let (first, values) = knots.basis_of_degree::<P>(u);
let points = &poles.points[first..=first + P];
match poles.weights {
None => {
let mut sum = [
values[0] * points[0].x,
values[0] * points[0].y,
values[0] * points[0].z,
];
for j in 1..=P {
sum[0] += values[j] * points[j].x;
sum[1] += values[j] * points[j].y;
sum[2] += values[j] * points[j].z;
}
Point::new(sum[0], sum[1], sum[2])
}
Some(weights) => {
let weights = &weights[first..=first + P];
let b = values[0] * weights[0];
let mut sum = [b * points[0].x, b * points[0].y, b * points[0].z, b];
for j in 1..=P {
let b = values[j] * weights[j];
sum[0] += b * points[j].x;
sum[1] += b * points[j].y;
sum[2] += b * points[j].z;
sum[3] += b;
}
let inverse = 1.0 / sum[3];
Point::new(sum[0] * inverse, sum[1] * inverse, sum[2] * inverse)
}
}
}
#[inline(always)]
fn basis_values_of<const P: usize>(left: &[f64; 4], right: &[f64; 4]) -> [f64; 4] {
let mut out = [1.0, 0.0, 0.0, 0.0];
for j in 1..=P {
let mut saved = 0.0;
for r in 0..P {
if r < j {
let temp = out[r] * (1.0 / (right[r + 1] + left[j - r]));
out[r] = if r == 0 {
right[1] * temp
} else {
saved + right[r + 1] * temp
};
saved = left[j - r] * temp;
}
}
out[j] = saved;
}
out
}
#[inline]
fn point_sized<const N: usize>(knots: &KnotVector, poles: Poles, u: f64) -> Point {
let mut values = [0.0f64; N];
let first = knots.basis(u, &mut values);
let degree = knots.degree;
let mut sum = [0.0f64; 4];
match poles.weights {
None => {
for (j, &b) in values.iter().enumerate().take(degree + 1) {
let p = poles.points[knots.pole_index(first, j)];
sum[0] += b * p.x;
sum[1] += b * p.y;
sum[2] += b * p.z;
}
Point::new(sum[0], sum[1], sum[2])
}
Some(weights) => {
for (j, &b) in values.iter().enumerate().take(degree + 1) {
let i = knots.pole_index(first, j);
let (p, b) = (poles.points[i], b * weights[i]);
sum[0] += b * p.x;
sum[1] += b * p.y;
sum[2] += b * p.z;
sum[3] += b;
}
let inverse = 1.0 / sum[3];
Point::new(sum[0] * inverse, sum[1] * inverse, sum[2] * inverse)
}
}
}
#[inline]
pub(crate) fn derivatives<const ORDER: usize>(
knots: &KnotVector,
poles: Poles,
u: f64,
) -> [Vector; 3] {
if knots.degree < SMALL {
derivatives_sized::<SMALL, ORDER>(knots, poles, u, None)
} else {
derivatives_sized::<LARGE, ORDER>(knots, poles, u, None)
}
}
#[inline]
pub(crate) fn derivatives_sided<const ORDER: usize>(
knots: &KnotVector,
poles: Poles,
u: f64,
side: Side,
) -> [Vector; 3] {
if knots.degree < SMALL {
derivatives_sized::<SMALL, ORDER>(knots, poles, u, Some(side))
} else {
derivatives_sized::<LARGE, ORDER>(knots, poles, u, Some(side))
}
}
#[inline]
fn derivatives_sized<const N: usize, const ORDER: usize>(
knots: &KnotVector,
poles: Poles,
u: f64,
side: Option<Side>,
) -> [Vector; 3] {
debug_assert!(
ORDER <= MAX_DERIVATIVE,
"derivative order {ORDER} not supported"
);
let mut w = Window::<N>::new();
knots.window_sided(u, ORDER, side, &mut w);
let mut sums = [[0.0f64; 4]; MAX_DERIVATIVE + 1];
for j in 0..=knots.degree {
let i = w.pole(j);
let p = poles.points[i];
let weight = poles.weights.map_or(1.0, |weights| weights[i]);
for (k, sum) in sums.iter_mut().enumerate().take(ORDER + 1) {
let b = w.ders[k][j] * weight;
sum[0] += b * p.x;
sum[1] += b * p.y;
sum[2] += b * p.z;
sum[3] += b;
}
}
let vector = |s: [f64; 4]| Vector::new(s[0], s[1], s[2]);
let mut out = sums.map(vector);
if poles.weights.is_some() {
let [w0, w1, w2] = sums.map(|s| s[3]);
let inverse = 1.0 / w0;
out[0] = out[0] * inverse;
if ORDER >= 1 {
out[1] = (out[1] - w1 * out[0]) * inverse;
}
if ORDER >= 2 {
out[2] = (out[2] - 2.0 * w1 * out[1] - w2 * out[0]) * inverse;
}
}
out
}
pub(crate) fn regions_at(
regions: &Option<Vec<(f64, f64)>>,
t: f64,
period: Option<f64>,
) -> Vec<(Option<(f64, f64)>, f64)> {
let Some(regions) = regions else {
return vec![(None, t)];
};
let mut out = Vec::new();
for &(a, b) in regions {
let shifted = std::iter::once(t).chain(period.map(|p| t + p));
if let Some(x) = shifted.into_iter().find(|&x| a <= x && x <= b) {
out.push((Some((a, b)), x));
}
}
out
}
pub(crate) fn inward(x: f64, bounds: Option<(f64, f64)>) -> f64 {
match bounds {
Some((_, hi)) if x >= hi => x.next_down(),
_ => x,
}
}
pub(crate) fn span_boxes(knots: &KnotVector, poles: Poles, breaks: &[f64]) -> Vec<[f64; 6]> {
fn boxes<const N: usize>(knots: &KnotVector, poles: Poles, breaks: &[f64]) -> Vec<[f64; 6]> {
let (mut left, mut right) = ([0.0f64; N], [0.0f64; N]);
breaks
.windows(2)
.map(|span| {
let first = knots.span_knots(0.5 * (span[0] + span[1]), &mut left, &mut right);
let mut b = [
f64::INFINITY,
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for j in 0..=knots.degree {
let p = poles.points[wrap(first + j, knots.pole_count, knots.periodic)];
for (c, x) in [p.x, p.y, p.z].into_iter().enumerate() {
b[c] = b[c].min(x);
b[c + 3] = b[c + 3].max(x);
}
}
b
})
.collect()
}
if knots.degree < SMALL {
boxes::<SMALL>(knots, poles, breaks)
} else {
boxes::<LARGE>(knots, poles, breaks)
}
}
pub(crate) fn span_bezier(knots: &KnotVector, poles: Poles, u: f64, out: &mut [[f64; 4]]) {
fn bezier<const N: usize>(knots: &KnotVector, poles: Poles, u: f64, out: &mut [[f64; 4]]) {
let degree = knots.degree;
let mut matrix = [[0.0f64; N]; N];
let first = knots.bezier_matrix(u, &mut matrix);
let mut local = [[0.0f64; 4]; N];
for (j, pole) in local.iter_mut().enumerate().take(degree + 1) {
*pole = poles.homogeneous(wrap(first + j, knots.pole_count, knots.periodic));
}
for (point, row) in out.iter_mut().zip(&matrix).take(degree + 1) {
*point = [0.0; 4];
for (&m, pole) in row.iter().zip(&local).take(degree + 1) {
for c in 0..4 {
point[c] += m * pole[c];
}
}
}
}
if knots.degree < SMALL {
bezier::<SMALL>(knots, poles, u, out);
} else {
bezier::<LARGE>(knots, poles, u, out);
}
}
pub(crate) fn validate_direction(
degree: usize,
n_poles: usize,
knots: &[f64],
mults: &[u32],
periodic: bool,
) -> Result<(), BSplineConstructionError> {
if !(1..=MAX_DEGREE).contains(°ree) {
return Err(BSplineConstructionError::InvalidDegree);
}
if knots.len() < 2 || mults.len() < 2 || knots.len() != mults.len() {
return Err(BSplineConstructionError::TooFewKnots);
}
for pair in knots.windows(2) {
if pair[1] <= pair[0] {
return Err(BSplineConstructionError::KnotsNotIncreasing);
}
}
let last = mults.len() - 1;
let deg = degree as u32;
if periodic {
if mults[0] != mults[last] {
return Err(BSplineConstructionError::PeriodicEndMultiplicityMismatch);
}
if mults.iter().any(|&m| m > deg) {
return Err(BSplineConstructionError::MultiplicityTooLarge);
}
} else {
if mults[0] > deg + 1 || mults[last] > deg + 1 {
return Err(BSplineConstructionError::MultiplicityTooLarge);
}
if mults[1..last].iter().any(|&m| m > deg) {
return Err(BSplineConstructionError::MultiplicityTooLarge);
}
}
let sum_mults: u32 = mults.iter().sum();
let expected = if periodic {
sum_mults as i64 - mults[last] as i64
} else {
sum_mults as i64 - degree as i64 - 1
};
if expected < 0 || n_poles as i64 != expected {
return Err(BSplineConstructionError::PoleCountMismatch);
}
Ok(())
}
fn knot_continuity(degree: usize, multiplicity: u32) -> Continuity {
Continuity::Order((degree as u32).saturating_sub(multiplicity))
}
pub(crate) fn continuity_at(
degree: usize,
knots: &[f64],
multiplicities: &[u32],
bounds: (f64, f64),
periodic: bool,
u: f64,
tol: Tolerance,
) -> Continuity {
let (first, last) = bounds;
let u = if periodic {
in_period(u, first, last)
} else {
u
};
let on_knot = knots
.iter()
.zip(multiplicities)
.find(|(knot, _)| (u - **knot).abs() <= tol.parametric * (1.0 + knot.abs()));
match on_knot {
Some((&knot, _)) if !periodic && (knot <= first || knot >= last) => Continuity::Smooth,
Some((_, &multiplicity)) => knot_continuity(degree, multiplicity),
None => Continuity::Smooth,
}
}
pub(crate) fn min_continuity(
degree: usize,
knots: &[f64],
multiplicities: &[u32],
bounds: (f64, f64),
periodic: bool,
) -> Continuity {
if periodic {
let period = bounds.1 - bounds.0;
return min_continuity_within(degree, knots, multiplicities, Some(period), bounds)
.min(knot_continuity(degree, multiplicities[0]));
}
min_continuity_within(degree, knots, multiplicities, None, bounds)
}
pub(crate) fn min_continuity_within(
degree: usize,
knots: &[f64],
multiplicities: &[u32],
period: Option<f64>,
(first, last): (f64, f64),
) -> Continuity {
knots
.iter()
.zip(multiplicities)
.filter(|(knot, _)| {
let shifted = match period {
Some(period) => first + (**knot - first).rem_euclid(period),
None => **knot,
};
first < shifted && shifted < last
})
.map(|(_, &multiplicity)| knot_continuity(degree, multiplicity))
.min()
.unwrap_or(Continuity::Smooth)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Continuity::{Order, Smooth};
#[test]
fn test_continuity_wraps_a_periodic_parameter_into_the_period() {
let (knots, multiplicities) = ([0.0, 1.0, 2.0, 3.0, 4.0], [1, 2, 1, 1, 1]);
let tol = Tolerance::DEFAULT;
let at = |u| continuity_at(2, &knots, &multiplicities, (0.0, 4.0), true, u, tol);
assert_eq!(at(1.0), Order(0));
assert_eq!(at(5.0), Order(0));
assert_eq!(at(-3.0), Order(0));
assert_eq!(at(0.0), Order(1));
assert_eq!(at(4.0), Order(1));
assert_eq!(at(-1e-12), Order(1));
assert_eq!(at(0.5), Smooth);
assert_eq!(
min_continuity(2, &knots, &multiplicities, (0.0, 4.0), true),
Order(0)
);
let seam = [2, 1, 1, 1, 2];
assert_eq!(min_continuity(2, &knots, &seam, (0.0, 4.0), true), Order(0));
assert_eq!(
min_continuity(2, &knots, &[1, 1, 1, 1, 1], (0.0, 4.0), true),
Order(1)
);
}
#[test]
fn test_continuity_ignores_knots_outside_an_unclamped_range() {
let knots = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let multiplicities = [2, 1, 1, 1, 1, 1, 1];
let tol = Tolerance::DEFAULT;
assert_eq!(
min_continuity(2, &knots, &multiplicities, (1.0, 4.0), false),
Order(1)
);
let at = |u| continuity_at(2, &knots, &multiplicities, (1.0, 4.0), false, u, tol);
assert_eq!(at(0.0), Smooth);
assert_eq!(at(1.0), Smooth);
assert_eq!(at(2.0), Order(1));
assert_eq!(at(4.0), Smooth);
}
#[test]
fn test_continuity_tolerance_is_relative_to_the_knot() {
let knots = [0.0, 1e6, 2e6];
let multiplicities = [2, 1, 2];
let tol = Tolerance::DEFAULT;
let at = |u| continuity_at(1, &knots, &multiplicities, (0.0, 2e6), false, u, tol);
assert_eq!(at(1e6 + 1e-4), Order(0));
assert_eq!(at(1e6 + 1e-2), Smooth);
assert_eq!(at(f64::NAN), Smooth);
}
#[test]
fn test_min_continuity_within_shifts_periodic_knots_into_the_range() {
let (knots, multiplicities) = ([0.0, 1.0, 2.0, 3.0, 4.0], [1, 2, 1, 1, 1]);
let within = |range| min_continuity_within(2, &knots, &multiplicities, Some(4.0), range);
assert_eq!(within((4.5, 5.5)), Order(0));
assert_eq!(within((1.5, 2.5)), Order(1));
assert_eq!(within((1.0, 1.9)), Smooth);
assert_eq!(within((-0.5, 0.5)), Order(1));
}
#[test]
fn test_basis_second_matches_general_derivatives() {
for p in 1..=9usize {
let flat: Vec<f64> = (0..2 * p + 2)
.map(|i| (i as f64).powf(1.3) + 0.1 * (i % 3) as f64)
.collect();
let span = p;
for step in 0..=10 {
let u = flat[span] + (flat[span + 1] - flat[span]) * step as f64 / 10.0;
let (mut left, mut right) = ([0.0; LARGE], [0.0; LARGE]);
for j in 1..=p {
left[j] = u - flat[span + 1 - j];
right[j] = flat[span + j] - u;
}
let mut fast = [[0.0; LARGE]; MAX_DERIVATIVE + 1];
let mut general = [[0.0; LARGE]; MAX_DERIVATIVE + 1];
basis_second(p, &left, &right, &mut fast);
basis_derivatives(p, &left, &right, 2, &mut general);
for k in 0..=2 {
for j in 0..=p {
let scale = general[k][..=p].iter().fold(1.0f64, |m, x| m.max(x.abs()));
assert!(
(fast[k][j] - general[k][j]).abs() <= 1e-13 * scale,
"degree {p}, order {k}, function {j}: {} vs {}",
fast[k][j],
general[k][j]
);
}
}
}
}
}
#[test]
fn test_flat_knots_repeats() {
assert_eq!(
flat_knots(&[0.0, 1.0, 2.0], &[3, 1, 3]),
vec![0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0]
);
}
#[test]
fn test_flat_knots_all_single() {
assert_eq!(
flat_knots(&[0.0, 1.0, 2.0], &[1, 1, 1]),
vec![0.0, 1.0, 2.0]
);
}
fn points(coordinates: &[[f64; 3]]) -> Vec<Point> {
coordinates
.iter()
.map(|c| Point::new(c[0], c[1], c[2]))
.collect()
}
fn knot_vector(
degree: usize,
knots: &[f64],
mults: &[u32],
periodic: bool,
count: usize,
) -> KnotVector {
KnotVector::new(degree, knots.to_vec(), mults.to_vec(), periodic, count).unwrap()
}
fn components(v: Vector) -> [f64; 3] {
[v.x, v.y, v.z]
}
#[test]
fn test_degree1_line_midpoint_and_derivative() {
let poles = points(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
let knots = knot_vector(1, &[0.0, 1.0], &[2, 2], false, 2);
let view = Poles {
points: &poles,
weights: None,
};
let [c, d1, _] = derivatives::<1>(&knots, view, 0.5);
assert_close(&components(c), &[1.0, 0.0, 0.0]);
assert_close(&components(d1), &[2.0, 0.0, 0.0]);
}
#[test]
fn test_degree2_bezier_by_hand() {
let poles = points(&[[0.0, 0.0, 0.0], [1.0, 2.0, 0.0], [2.0, 0.0, 0.0]]);
let knots = knot_vector(2, &[0.0, 1.0], &[3, 3], false, 3);
let view = Poles {
points: &poles,
weights: None,
};
let [c, d1, _] = derivatives::<2>(&knots, view, 0.5);
assert_close(&components(c), &[1.0, 1.0, 0.0]);
assert_close(&components(d1), &[2.0, 0.0, 0.0]);
let p = point(&knots, view, 0.5);
assert_close(&[p.x, p.y, p.z], &[1.0, 1.0, 0.0]);
}
#[test]
fn test_point_of_low_degree_matches_general_path() {
for degree in 1..=3usize {
let double = degree.min(2) as u32;
let knots = knot_vector(
degree,
&[0.0, 0.3, 0.45, 1.2, 2.0],
&[degree as u32 + 1, 1, double, 1, degree as u32 + 1],
false,
degree + 3 + double as usize,
);
let poles: Vec<Point> = (0..knots.pole_count)
.map(|i| {
let t = i as f64;
Point::new(t, (1.7 * t).sin(), 0.3 * t * t)
})
.collect();
let weights: Vec<f64> = (0..knots.pole_count)
.map(|i| 0.5 + 0.3 * (i % 3) as f64)
.collect();
for weights in [None, Some(weights.as_slice())] {
let view = Poles {
points: &poles,
weights,
};
for step in -2..=42 {
let u = step as f64 / 20.0;
let p = point(&knots, view, u);
let [c, _, _] = derivatives::<0>(&knots, view, u);
assert_close(&[p.x, p.y, p.z], &components(c));
}
}
}
}
#[test]
fn test_rational_quarter_circle_on_unit_circle() {
let w = std::f64::consts::FRAC_1_SQRT_2; let poles = points(&[[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]);
let weights = [1.0, w, 1.0];
let knots = knot_vector(2, &[0.0, 1.0], &[3, 3], false, 3);
let view = Poles {
points: &poles,
weights: Some(&weights),
};
for t in [0.0, 0.25, 0.5, 1.0] {
let p = point(&knots, view, t);
let r = (p.x * p.x + p.y * p.y).sqrt();
assert!((r - 1.0).abs() < 1e-12, "t={t} radius={r}");
let [c, d1, _] = derivatives::<1>(&knots, view, t);
assert_close(&components(c), &[p.x, p.y, p.z]);
assert!((c.x * d1.x + c.y * d1.y).abs() < 1e-12, "t={t}");
}
let p = point(&knots, view, 1.0);
assert_close(&[p.x, p.y, p.z], &[0.0, 1.0, 0.0]);
}
#[test]
fn test_periodic_value_repeats_over_period() {
let poles = periodic_ring_poles();
let knots = knot_vector(3, &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1; 7], true, 6);
let view = Poles {
points: &poles,
weights: None,
};
for u in [0.3, 1.7, 4.9] {
let a = derivatives::<2>(&knots, view, u);
let b = derivatives::<2>(&knots, view, u + 6.0);
for k in 0..3 {
assert_close(&components(a[k]), &components(b[k]));
}
}
}
fn assert_bezier_matches(
degree: usize,
periodic: bool,
knots: &[f64],
mults: &[u32],
poles: &[Point],
weights: Option<&[f64]>,
) {
let vector = knot_vector(degree, knots, mults, periodic, poles.len());
let view = Poles {
points: poles,
weights,
};
let binomial =
|n: usize, k: usize| (0..k).fold(1.0, |c, i| c * (n - i) as f64 / (i + 1) as f64);
for span in knots.windows(2) {
let (a, b) = (span[0], span[1]);
let mut bezier = vec![[0.0; 4]; degree + 1];
span_bezier(&vector, view, 0.5 * (a + b), &mut bezier);
for s in [0.0, 0.2, 0.5, 0.9, 1.0] {
let expected = point(&vector, view, a + s * (b - a));
let mut sum = [0.0; 4];
for (k, x) in bezier.iter().enumerate() {
let basis = binomial(degree, k)
* s.powi(k as i32)
* (1.0 - s).powi((degree - k) as i32);
for c in 0..4 {
sum[c] += basis * x[c];
}
}
let got = [sum[0] / sum[3], sum[1] / sum[3], sum[2] / sum[3]];
for (g, e) in got.into_iter().zip([expected.x, expected.y, expected.z]) {
assert!((g - e).abs() < 1e-12, "span [{a}, {b}] s {s}: {g} vs {e}");
}
}
}
}
#[test]
fn test_span_bezier_nonuniform_with_interior_multiplicity() {
let poles: Vec<Point> = (0..8)
.map(|i| Point::new(i as f64, (i * i % 5) as f64, (i % 3) as f64 - 1.0))
.collect();
assert_bezier_matches(
3,
false,
&[0.0, 0.7, 1.0, 2.5, 4.0],
&[4, 1, 2, 1, 4],
&poles,
None,
);
}
#[test]
fn test_span_bezier_rational_homogeneous() {
let weights = [1.0, 0.4, 2.5, 1.2, 0.8, 3.0];
let poles: Vec<Point> = (0..6)
.map(|i| Point::new(i as f64, ((i * 7) % 4) as f64, -(i as f64)))
.collect();
assert_bezier_matches(
2,
false,
&[-1.0, 0.5, 1.5, 3.0, 3.5],
&[3, 1, 1, 1, 3],
&poles,
Some(&weights),
);
}
#[test]
fn test_span_bezier_periodic_with_seam_multiplicity() {
let poles = periodic_ring_poles();
assert_bezier_matches(
3,
true,
&[0.0, 1.0, 2.0, 3.5, 4.0, 5.0, 6.0],
&[1, 1, 1, 1, 1, 1, 1],
&poles,
None,
);
assert_bezier_matches(
3,
true,
&[0.0, 1.5, 3.0, 4.0, 5.0, 6.0],
&[2, 1, 1, 1, 1, 2],
&poles,
None,
);
}
fn periodic_ring_poles() -> Vec<Point> {
points(&[
[2.0, 0.0, 0.3],
[1.0000000000000002, 1.7320508075688772, -0.3],
[-0.9999999999999996, 1.7320508075688774, 0.3],
[-2.0, 2.4492935982947064e-16, -0.3],
[-1.0000000000000009, -1.7320508075688767, 0.3],
[1.0000000000000002, -1.7320508075688772, -0.3],
])
}
#[test]
fn test_validation_ok_nonperiodic() {
assert_eq!(
validate_direction(3, 6, &[0.0, 1.0, 2.0, 3.0], &[4, 1, 1, 4], false),
Ok(())
);
}
#[test]
fn test_validation_ok_periodic() {
assert_eq!(
validate_direction(3, 6, &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1; 7], true),
Ok(())
);
}
#[test]
fn test_validation_invalid_degree_low() {
assert_eq!(
validate_direction(0, 1, &[0.0, 1.0], &[1, 1], false),
Err(BSplineConstructionError::InvalidDegree)
);
}
#[test]
fn test_validation_invalid_degree_high() {
assert_eq!(
validate_direction(26, 1, &[0.0, 1.0], &[1, 1], false),
Err(BSplineConstructionError::InvalidDegree)
);
}
#[test]
fn test_validation_too_few_knots() {
assert_eq!(
validate_direction(1, 1, &[0.0], &[2], false),
Err(BSplineConstructionError::TooFewKnots)
);
}
#[test]
fn test_validation_knots_not_increasing() {
assert_eq!(
validate_direction(1, 2, &[1.0, 1.0], &[2, 2], false),
Err(BSplineConstructionError::KnotsNotIncreasing)
);
}
#[test]
fn test_validation_multiplicity_too_large_interior() {
assert_eq!(
validate_direction(2, 3, &[0.0, 1.0, 2.0], &[3, 3, 3], false),
Err(BSplineConstructionError::MultiplicityTooLarge)
);
}
#[test]
fn test_validation_multiplicity_too_large_end() {
assert_eq!(
validate_direction(3, 3, &[0.0, 1.0], &[5, 3], false),
Err(BSplineConstructionError::MultiplicityTooLarge)
);
}
#[test]
fn test_validation_periodic_mult_too_large() {
assert_eq!(
validate_direction(2, 4, &[0.0, 1.0, 2.0, 3.0], &[3, 1, 1, 3], true),
Err(BSplineConstructionError::MultiplicityTooLarge)
);
}
#[test]
fn test_validation_periodic_end_mismatch() {
assert_eq!(
validate_direction(3, 6, &[0.0, 1.0, 2.0, 3.0], &[2, 1, 1, 3], true),
Err(BSplineConstructionError::PeriodicEndMultiplicityMismatch)
);
}
#[test]
fn test_validation_pole_count_mismatch_nonperiodic() {
assert_eq!(
validate_direction(3, 5, &[0.0, 1.0, 2.0, 3.0], &[4, 1, 1, 4], false),
Err(BSplineConstructionError::PoleCountMismatch)
);
}
#[test]
fn test_validation_pole_count_mismatch_periodic() {
assert_eq!(
validate_direction(3, 7, &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1; 7], true),
Err(BSplineConstructionError::PoleCountMismatch)
);
}
#[test]
fn test_weight_error_variants_display() {
assert!(
!BSplineConstructionError::WeightCountMismatch
.to_string()
.is_empty()
);
assert!(
!BSplineConstructionError::NonPositiveWeight
.to_string()
.is_empty()
);
}
#[test]
fn test_fixture_replay_all_cases() {
replay_fixture();
}
fn assert_close(actual: &[f64], expected: &[f64]) {
assert_eq!(actual.len(), expected.len());
for (i, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
let tol = 1e-9 * f64::max(1.0, e.abs());
assert!((a - e).abs() <= tol, "index {i}: {a} vs {e}");
}
}
use serde_json::Value;
fn replay_fixture() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/curves_bspline.json"
);
let text = std::fs::read_to_string(path).expect("read fixture");
let json: Value = serde_json::from_str(&text).expect("parse fixture");
let cases = json["cases"].as_array().expect("cases array");
for case in cases {
replay_case(case);
}
}
fn arr_f64(v: &Value) -> Vec<f64> {
v.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap())
.collect()
}
fn replay_case(case: &Value) {
let name = case["name"].as_str().unwrap();
let degree = case["degree"].as_u64().unwrap() as usize;
let periodic = case["periodic"].as_bool().unwrap();
let knots = arr_f64(&case["knots"]);
let mults: Vec<u32> = case["mults"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_u64().unwrap() as u32)
.collect();
let pole_rows: Vec<Vec<f64>> = case["poles"]
.as_array()
.unwrap()
.iter()
.map(arr_f64)
.collect();
let weights: Option<Vec<f64>> = case
.get("weights")
.and_then(|w| if w.is_null() { None } else { Some(arr_f64(w)) });
let poles: Vec<Point> = pole_rows
.iter()
.map(|p| Point::new(p[0], p[1], p[2]))
.collect();
let vector = KnotVector::new(degree, knots, mults, periodic, poles.len()).unwrap();
let view = Poles {
points: &poles,
weights: weights.as_deref(),
};
for sample in case["samples"].as_array().unwrap() {
let u = sample["u"].as_f64().unwrap();
let all = derivatives::<2>(&vector, view, u);
let p = point(&vector, view, u);
let euc: Vec<f64> = all.iter().flat_map(|v| [v.x, v.y, v.z]).collect();
for (c, (&a, b)) in euc[..3].iter().zip([p.x, p.y, p.z]).enumerate() {
assert!(
(a - b).abs() <= 1e-12 * f64::max(1.0, b.abs()),
"{name} u={u} point[{c}]"
);
}
for (key, idx) in [("point", 0usize), ("d1", 1), ("d2", 2)] {
let expected = arr_f64(&sample[key]);
let got = &euc[idx * 3..idx * 3 + 3];
for (c, (&g, &e)) in got.iter().zip(expected.iter()).enumerate() {
let tol = 1e-7 * f64::max(1.0, e.abs());
assert!(
(g - e).abs() <= tol,
"{name} u={u} {key}[{c}]: got {g}, expected {e} (tol {tol})"
);
}
}
}
}
}