use crate::error::{InterpolateError, InterpolateResult};
use scirs2_core::ndarray::{Array2, Array3};
pub type BSplineResult<T> = InterpolateResult<T>;
#[derive(Debug, Clone)]
pub struct BSplineSurface {
control_pts: Array3<f64>,
u_knots: Vec<f64>,
v_knots: Vec<f64>,
p: usize,
q: usize,
}
impl BSplineSurface {
pub fn new(
control_pts: Array3<f64>,
u_knots: Vec<f64>,
v_knots: Vec<f64>,
p: usize,
q: usize,
) -> BSplineResult<Self> {
let shape = control_pts.shape();
if shape.len() != 3 || shape[2] != 3 {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface: control_pts must have shape (m+1, n+1, 3); \
got {:?}",
shape
),
});
}
if p == 0 {
return Err(InterpolateError::InvalidInput {
message: "BSplineSurface: degree p must be >= 1".into(),
});
}
if q == 0 {
return Err(InterpolateError::InvalidInput {
message: "BSplineSurface: degree q must be >= 1".into(),
});
}
let m = shape[0] - 1; let n = shape[1] - 1;
let expected_u = m + p + 2;
let expected_v = n + q + 2;
if u_knots.len() != expected_u {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface: u_knots.len()={} but expected {}=(m+p+2) \
with m={}, p={}",
u_knots.len(),
expected_u,
m,
p
),
});
}
if v_knots.len() != expected_v {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface: v_knots.len()={} but expected {}=(n+q+2) \
with n={}, q={}",
v_knots.len(),
expected_v,
n,
q
),
});
}
for i in 1..u_knots.len() {
if u_knots[i] < u_knots[i - 1] {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface: u_knots must be non-decreasing; \
u_knots[{i}]={} < u_knots[{}]={}",
u_knots[i], i-1, u_knots[i-1]
),
});
}
}
for i in 1..v_knots.len() {
if v_knots[i] < v_knots[i - 1] {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface: v_knots must be non-decreasing; \
v_knots[{i}]={} < v_knots[{}]={}",
v_knots[i], i-1, v_knots[i-1]
),
});
}
}
Ok(Self {
control_pts,
u_knots,
v_knots,
p,
q,
})
}
pub fn evaluate(&self, u: f64, v: f64) -> BSplineResult<[f64; 3]> {
let (u_min, u_max) = domain_bounds(&self.u_knots, self.p);
let (v_min, v_max) = domain_bounds(&self.v_knots, self.q);
let u_clamped = clamp_to_domain(u, u_min, u_max);
let v_clamped = clamp_to_domain(v, v_min, v_max);
let m = self.control_pts.shape()[0] - 1;
let n = self.control_pts.shape()[1] - 1;
let n_u = basis_functions(&self.u_knots, u_clamped, self.p, m);
let n_v = basis_functions(&self.v_knots, v_clamped, self.q, n);
let span_u = find_knot_span(&self.u_knots, u_clamped, self.p, m);
let span_v = find_knot_span(&self.v_knots, v_clamped, self.q, n);
let mut pt = [0.0_f64; 3];
for (li, &ni) in n_u.iter().enumerate() {
let i = span_u as isize - self.p as isize + li as isize;
if i < 0 || i > m as isize {
continue;
}
let i = i as usize;
for (lj, &nj) in n_v.iter().enumerate() {
let j = span_v as isize - self.q as isize + lj as isize;
if j < 0 || j > n as isize {
continue;
}
let j = j as usize;
let w = ni * nj;
pt[0] += w * self.control_pts[[i, j, 0]];
pt[1] += w * self.control_pts[[i, j, 1]];
pt[2] += w * self.control_pts[[i, j, 2]];
}
}
Ok(pt)
}
pub fn normal(&self, u: f64, v: f64) -> BSplineResult<[f64; 3]> {
let du = self.partial_derivative_u(u, v)?;
let dv = self.partial_derivative_v(u, v)?;
let nx = du[1] * dv[2] - du[2] * dv[1];
let ny = du[2] * dv[0] - du[0] * dv[2];
let nz = du[0] * dv[1] - du[1] * dv[0];
Ok([nx, ny, nz])
}
pub fn partial_derivative_u(&self, u: f64, v: f64) -> BSplineResult<[f64; 3]> {
let (u_min, u_max) = domain_bounds(&self.u_knots, self.p);
let (v_min, v_max) = domain_bounds(&self.v_knots, self.q);
let u_c = clamp_to_domain(u, u_min, u_max);
let v_c = clamp_to_domain(v, v_min, v_max);
let m = self.control_pts.shape()[0] - 1;
let n = self.control_pts.shape()[1] - 1;
let dn_u = basis_functions_derivative(&self.u_knots, u_c, self.p, m);
let n_v = basis_functions(&self.v_knots, v_c, self.q, n);
let span_u = find_knot_span(&self.u_knots, u_c, self.p, m);
let span_v = find_knot_span(&self.v_knots, v_c, self.q, n);
let mut dpt = [0.0_f64; 3];
for (li, &dni) in dn_u.iter().enumerate() {
let i = span_u as isize - self.p as isize + li as isize;
if i < 0 || i > m as isize {
continue;
}
let i = i as usize;
for (lj, &nj) in n_v.iter().enumerate() {
let j = span_v as isize - self.q as isize + lj as isize;
if j < 0 || j > n as isize {
continue;
}
let j = j as usize;
let w = dni * nj;
dpt[0] += w * self.control_pts[[i, j, 0]];
dpt[1] += w * self.control_pts[[i, j, 1]];
dpt[2] += w * self.control_pts[[i, j, 2]];
}
}
Ok(dpt)
}
pub fn partial_derivative_v(&self, u: f64, v: f64) -> BSplineResult<[f64; 3]> {
let (u_min, u_max) = domain_bounds(&self.u_knots, self.p);
let (v_min, v_max) = domain_bounds(&self.v_knots, self.q);
let u_c = clamp_to_domain(u, u_min, u_max);
let v_c = clamp_to_domain(v, v_min, v_max);
let m = self.control_pts.shape()[0] - 1;
let n = self.control_pts.shape()[1] - 1;
let n_u = basis_functions(&self.u_knots, u_c, self.p, m);
let dn_v = basis_functions_derivative(&self.v_knots, v_c, self.q, n);
let span_u = find_knot_span(&self.u_knots, u_c, self.p, m);
let span_v = find_knot_span(&self.v_knots, v_c, self.q, n);
let mut dpt = [0.0_f64; 3];
for (li, &ni) in n_u.iter().enumerate() {
let i = span_u as isize - self.p as isize + li as isize;
if i < 0 || i > m as isize {
continue;
}
let i = i as usize;
for (lj, &dnj) in dn_v.iter().enumerate() {
let j = span_v as isize - self.q as isize + lj as isize;
if j < 0 || j > n as isize {
continue;
}
let j = j as usize;
let w = ni * dnj;
dpt[0] += w * self.control_pts[[i, j, 0]];
dpt[1] += w * self.control_pts[[i, j, 1]];
dpt[2] += w * self.control_pts[[i, j, 2]];
}
}
Ok(dpt)
}
pub fn interpolate_grid(
points: &Array3<f64>,
p: usize,
q: usize,
) -> BSplineResult<Self> {
let shape = points.shape();
if shape.len() != 3 || shape[2] != 3 {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface::interpolate_grid: points must have shape (r+1,s+1,3); \
got {:?}",
shape
),
});
}
if p == 0 || q == 0 {
return Err(InterpolateError::InvalidInput {
message: "BSplineSurface::interpolate_grid: degrees must be >= 1".into(),
});
}
let r = shape[0] - 1; let s = shape[1] - 1;
if r < p {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface::interpolate_grid: need >= {} rows for degree p={}; \
got {}",
p + 1, p, r + 1
),
});
}
if s < q {
return Err(InterpolateError::InvalidInput {
message: format!(
"BSplineSurface::interpolate_grid: need >= {} cols for degree q={}; \
got {}",
q + 1, q, s + 1
),
});
}
let u_params = chord_params_u(points, r, s);
let v_params = chord_params_v(points, r, s);
let u_knots = clamped_knots_from_params(&u_params, p, r);
let v_knots = clamped_knots_from_params(&v_params, q, s);
let mut ctrl = Array3::<f64>::zeros((r + 1, s + 1, 3));
for j in 0..=s {
let data_col: Vec<[f64; 3]> = (0..=r)
.map(|i| [points[[i, j, 0]], points[[i, j, 1]], points[[i, j, 2]]])
.collect();
let c = spline_interp_1d(&u_params, &data_col, &u_knots, p, r)?;
for i in 0..=r {
ctrl[[i, j, 0]] = c[i][0];
ctrl[[i, j, 1]] = c[i][1];
ctrl[[i, j, 2]] = c[i][2];
}
}
let ctrl_temp = ctrl.clone();
for i in 0..=r {
let data_row: Vec<[f64; 3]> = (0..=s)
.map(|j| [ctrl_temp[[i, j, 0]], ctrl_temp[[i, j, 1]], ctrl_temp[[i, j, 2]]])
.collect();
let c = spline_interp_1d(&v_params, &data_row, &v_knots, q, s)?;
for j in 0..=s {
ctrl[[i, j, 0]] = c[j][0];
ctrl[[i, j, 1]] = c[j][1];
ctrl[[i, j, 2]] = c[j][2];
}
}
Self::new(ctrl, u_knots, v_knots, p, q)
}
pub fn n_ctrl_u(&self) -> usize {
self.control_pts.shape()[0]
}
pub fn n_ctrl_v(&self) -> usize {
self.control_pts.shape()[1]
}
pub fn degree_u(&self) -> usize {
self.p
}
pub fn degree_v(&self) -> usize {
self.q
}
pub fn u_domain(&self) -> (f64, f64) {
domain_bounds(&self.u_knots, self.p)
}
pub fn v_domain(&self) -> (f64, f64) {
domain_bounds(&self.v_knots, self.q)
}
}
fn find_knot_span(t: &[f64], u: f64, p: usize, n_ctrl: usize) -> usize {
let n = n_ctrl; let m = t.len() - 1;
if u >= t[m - p] {
let mut span = m - p - 1;
while span > p && t[span] >= t[span + 1] {
span -= 1;
}
return span;
}
let mut lo = p;
let mut hi = n + 1;
let mut mid = (lo + hi) / 2;
while u < t[mid] || u >= t[mid + 1] {
if u < t[mid] {
hi = mid;
} else {
lo = mid;
}
mid = (lo + hi) / 2;
if mid == lo {
break;
}
}
mid
}
fn basis_functions(t: &[f64], u: f64, p: usize, n_ctrl: usize) -> Vec<f64> {
let span = find_knot_span(t, u, p, n_ctrl);
let mut n = vec![0.0_f64; p + 1];
let mut left = vec![0.0_f64; p + 1];
let mut right = vec![0.0_f64; p + 1];
n[0] = 1.0;
for j in 1..=p {
left[j] = u - t[span + 1 - j];
right[j] = t[span + j] - u;
let mut saved = 0.0_f64;
for r in 0..j {
let denom = right[r + 1] + left[j - r];
let temp = if denom.abs() < 1e-300 {
0.0
} else {
n[r] / denom
};
n[r] = saved + right[r + 1] * temp;
saved = left[j - r] * temp;
}
n[j] = saved;
}
n
}
fn basis_functions_derivative(t: &[f64], u: f64, p: usize, n_ctrl: usize) -> Vec<f64> {
if p == 0 {
return vec![0.0];
}
let span = find_knot_span(t, u, p, n_ctrl);
let n_lower = basis_functions_at_span(t, u, p - 1, span);
let mut dn = vec![0.0_f64; p + 1];
let p_f = p as f64;
for r in 0..=p {
let i = span as isize - p as isize + r as isize;
let left_val = if i >= 0 {
let i = i as usize;
let denom = t.get(i + p).copied().unwrap_or(0.0)
- t.get(i).copied().unwrap_or(0.0);
if denom.abs() > 1e-300 && r < n_lower.len() {
n_lower[r] / denom
} else {
0.0
}
} else {
0.0
};
let right_val = if r + 1 < n_lower.len() {
let ip1 = (i + 1).max(0) as usize;
let denom = t.get(ip1 + p).copied().unwrap_or(0.0)
- t.get(ip1).copied().unwrap_or(0.0);
if denom.abs() > 1e-300 {
n_lower[r + 1] / denom
} else {
0.0
}
} else {
0.0
};
dn[r] = p_f * (left_val - right_val);
}
dn
}
fn basis_functions_at_span(t: &[f64], u: f64, p: usize, span: usize) -> Vec<f64> {
let mut n = vec![0.0_f64; p + 2]; let mut left = vec![0.0_f64; p + 1];
let mut right = vec![0.0_f64; p + 1];
n[0] = 1.0;
for j in 1..=p {
let l = if span + 1 >= j { t[span + 1 - j] } else { 0.0 };
let r = if span + j < t.len() { t[span + j] } else { 0.0 };
left[j] = u - l;
right[j] = r - u;
let mut saved = 0.0_f64;
for rr in 0..j {
let denom = right[rr + 1] + left[j - rr];
let temp = if denom.abs() < 1e-300 {
0.0
} else {
n[rr] / denom
};
n[rr] = saved + right[rr + 1] * temp;
saved = left[j - rr] * temp;
}
n[j] = saved;
}
n
}
fn domain_bounds(t: &[f64], p: usize) -> (f64, f64) {
let lo = t[p];
let hi = t[t.len() - 1 - p];
(lo, hi)
}
#[inline]
fn clamp_to_domain(u: f64, u_min: f64, u_max: f64) -> f64 {
let eps = f64::EPSILON * (u_max - u_min).abs();
u.max(u_min - eps).min(u_max + eps)
}
fn chord_params_u(points: &Array3<f64>, r: usize, s: usize) -> Vec<f64> {
let mut params = vec![0.0_f64; r + 1];
params[0] = 0.0;
for j in 0..=s {
let mut chord = vec![0.0_f64; r + 1];
chord[0] = 0.0;
let mut total = 0.0_f64;
for i in 0..r {
let dx = points[[i + 1, j, 0]] - points[[i, j, 0]];
let dy = points[[i + 1, j, 1]] - points[[i, j, 1]];
let dz = points[[i + 1, j, 2]] - points[[i, j, 2]];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
chord[i + 1] = chord[i] + dist;
total += dist;
}
if total < f64::EPSILON {
for i in 0..=r {
params[i] += i as f64 / r as f64;
}
} else {
for i in 0..=r {
params[i] += chord[i] / total;
}
}
}
let cols = (s + 1) as f64;
for p in params.iter_mut() {
*p /= cols;
}
params[r] = 1.0; params
}
fn chord_params_v(points: &Array3<f64>, r: usize, s: usize) -> Vec<f64> {
let mut params = vec![0.0_f64; s + 1];
params[0] = 0.0;
for i in 0..=r {
let mut chord = vec![0.0_f64; s + 1];
chord[0] = 0.0;
let mut total = 0.0_f64;
for j in 0..s {
let dx = points[[i, j + 1, 0]] - points[[i, j, 0]];
let dy = points[[i, j + 1, 1]] - points[[i, j, 1]];
let dz = points[[i, j + 1, 2]] - points[[i, j, 2]];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
chord[j + 1] = chord[j] + dist;
total += dist;
}
if total < f64::EPSILON {
for j in 0..=s {
params[j] += j as f64 / s as f64;
}
} else {
for j in 0..=s {
params[j] += chord[j] / total;
}
}
}
let rows = (r + 1) as f64;
for p in params.iter_mut() {
*p /= rows;
}
params[s] = 1.0;
params
}
fn clamped_knots_from_params(params: &[f64], p: usize, n: usize) -> Vec<f64> {
let m = n + p + 1; let mut t = vec![0.0_f64; m + 1];
for j in 0..=p {
t[j] = 0.0;
}
for j in m - p..=m {
t[j] = 1.0;
}
for j in 1..=n - p {
let mut sum = 0.0_f64;
for i in j..j + p {
sum += params.get(i).copied().unwrap_or(1.0);
}
t[j + p] = sum / p as f64;
}
t
}
fn spline_interp_1d(
params: &[f64],
data: &[[f64; 3]],
knots: &[f64],
p: usize,
n: usize,
) -> BSplineResult<Vec<[f64; 3]>> {
let n_pts = n + 1;
let mut bmat = vec![0.0_f64; n_pts * n_pts];
for i in 0..n_pts {
let u = params[i];
let span = find_knot_span(knots, u, p, n);
let basis = basis_functions(knots, u, p, n);
for (li, &bv) in basis.iter().enumerate() {
let j = span as isize - p as isize + li as isize;
if j >= 0 && (j as usize) < n_pts {
bmat[i * n_pts + j as usize] = bv;
}
}
}
let mut ctrl = vec![[0.0_f64; 3]; n_pts];
for coord in 0..3 {
let rhs: Vec<f64> = data.iter().map(|p| p[coord]).collect();
let sol = gauss_solve(&bmat, &rhs, n_pts)?;
for i in 0..n_pts {
ctrl[i][coord] = sol[i];
}
}
Ok(ctrl)
}
fn gauss_solve(a_flat: &[f64], b: &[f64], n: usize) -> BSplineResult<Vec<f64>> {
let mut aug = vec![0.0_f64; n * (n + 1)];
for i in 0..n {
for j in 0..n {
aug[i * (n + 1) + j] = a_flat[i * n + j];
}
aug[i * (n + 1) + n] = b[i];
}
for col in 0..n {
let mut max_val = aug[col * (n + 1) + col].abs();
let mut max_row = col;
for row in col + 1..n {
let v = aug[row * (n + 1) + col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-15 {
return Err(InterpolateError::LinalgError(
"BSplineSurface::interpolate_grid: singular collocation matrix; \
try different degree or more data points".into(),
));
}
if max_row != col {
for j in 0..=n {
aug.swap(col * (n + 1) + j, max_row * (n + 1) + j);
}
}
let pivot = aug[col * (n + 1) + col];
for row in col + 1..n {
let factor = aug[row * (n + 1) + col] / pivot;
for j in col..=n {
let delta = factor * aug[col * (n + 1) + j];
aug[row * (n + 1) + j] -= delta;
}
}
}
let mut x = vec![0.0_f64; n];
for col in (0..n).rev() {
let mut val = aug[col * (n + 1) + n];
for j in col + 1..n {
val -= aug[col * (n + 1) + j] * x[j];
}
x[col] = val / aug[col * (n + 1) + col];
}
Ok(x)
}
pub fn make_bilinear_patch(corners: [[f64; 3]; 4]) -> BSplineResult<BSplineSurface> {
let mut ctrl = Array3::<f64>::zeros((2, 2, 3));
for k in 0..3 {
ctrl[[0, 0, k]] = corners[0][k]; ctrl[[1, 0, k]] = corners[1][k]; ctrl[[0, 1, k]] = corners[2][k]; ctrl[[1, 1, k]] = corners[3][k]; }
let u_knots = vec![0.0, 0.0, 1.0, 1.0];
let v_knots = vec![0.0, 0.0, 1.0, 1.0];
BSplineSurface::new(ctrl, u_knots, v_knots, 1, 1)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::Array3;
fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
.collect()
}
fn flat_bilinear() -> BSplineSurface {
make_bilinear_patch([
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
])
.expect("test: should succeed")
}
#[test]
fn test_new_valid_bilinear() {
let surf = flat_bilinear();
assert_eq!(surf.degree_u(), 1);
assert_eq!(surf.degree_v(), 1);
assert_eq!(surf.n_ctrl_u(), 2);
assert_eq!(surf.n_ctrl_v(), 2);
}
#[test]
fn test_new_zero_degree_error() {
let ctrl = Array3::<f64>::zeros((2, 2, 3));
let result = BSplineSurface::new(
ctrl.clone(),
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
0, 1,
);
assert!(result.is_err());
}
#[test]
fn test_new_wrong_knot_length_error() {
let ctrl = Array3::<f64>::zeros((2, 2, 3));
let result = BSplineSurface::new(
ctrl,
vec![0.0, 0.5, 1.0], vec![0.0, 0.0, 1.0, 1.0],
1,
1,
);
assert!(result.is_err());
}
#[test]
fn test_new_non_monotone_knots_error() {
let ctrl = Array3::<f64>::zeros((2, 2, 3));
let result = BSplineSurface::new(
ctrl,
vec![0.0, 0.0, 1.0, 1.0],
vec![1.0, 0.0, 0.5, 1.0], 1,
1,
);
assert!(result.is_err());
}
#[test]
fn test_bilinear_corners() {
let surf = flat_bilinear();
let (u_min, u_max) = surf.u_domain();
let (v_min, v_max) = surf.v_domain();
let pt00 = surf.evaluate(u_min, v_min).expect("test: should succeed");
let pt10 = surf.evaluate(u_max, v_min).expect("test: should succeed");
let pt01 = surf.evaluate(u_min, v_max).expect("test: should succeed");
let pt11 = surf.evaluate(u_max, v_max).expect("test: should succeed");
assert_abs_diff_eq!(pt00[0], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(pt10[0], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(pt01[1], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(pt11[0], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(pt11[1], 1.0, epsilon = 1e-10);
}
#[test]
fn test_bilinear_center() {
let surf = flat_bilinear();
let pt = surf.evaluate(0.5, 0.5).expect("test: should succeed");
assert_abs_diff_eq!(pt[0], 0.5, epsilon = 1e-10);
assert_abs_diff_eq!(pt[1], 0.5, epsilon = 1e-10);
assert_abs_diff_eq!(pt[2], 0.0, epsilon = 1e-10);
}
#[test]
fn test_bilinear_linearity_in_u() {
let surf = flat_bilinear();
for v in [0.2, 0.5, 0.8] {
let p0 = surf.evaluate(0.0, v).expect("test: should succeed");
let p1 = surf.evaluate(1.0, v).expect("test: should succeed");
let pm = surf.evaluate(0.5, v).expect("test: should succeed");
assert_abs_diff_eq!(pm[0], (p0[0] + p1[0]) / 2.0, epsilon = 1e-9);
assert_abs_diff_eq!(pm[1], (p0[1] + p1[1]) / 2.0, epsilon = 1e-9);
}
}
#[test]
fn test_bilinear_normal_points_up() {
let surf = flat_bilinear();
let n = surf.normal(0.5, 0.5).expect("test: should succeed");
assert!(n[2].abs() > n[0].abs(), "Normal z should dominate");
assert!(n[2].abs() > n[1].abs(), "Normal z should dominate");
}
#[test]
fn test_normal_nonzero() {
let surf = flat_bilinear();
let n = surf.normal(0.3, 0.7).expect("test: should succeed");
let mag = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!(mag > 1e-10, "Normal magnitude should be non-zero");
}
#[test]
fn test_interpolate_grid_flat_plane() {
let r = 4;
let s = 4;
let mut pts = Array3::<f64>::zeros((r + 1, s + 1, 3));
for i in 0..=r {
for j in 0..=s {
pts[[i, j, 0]] = i as f64 / r as f64;
pts[[i, j, 1]] = j as f64 / s as f64;
pts[[i, j, 2]] = 0.0;
}
}
let surf = BSplineSurface::interpolate_grid(&pts, 3, 3).expect("test: should succeed");
let pt = surf.evaluate(0.5, 0.5).expect("test: should succeed");
assert_abs_diff_eq!(pt[2], 0.0, epsilon = 1e-8);
assert_abs_diff_eq!(pt[0], 0.5, epsilon = 0.1);
}
#[test]
fn test_interpolate_grid_linear_z() {
let r = 3;
let s = 3;
let mut pts = Array3::<f64>::zeros((r + 1, s + 1, 3));
for i in 0..=r {
for j in 0..=s {
let x = i as f64 / r as f64;
let y = j as f64 / s as f64;
pts[[i, j, 0]] = x;
pts[[i, j, 1]] = y;
pts[[i, j, 2]] = x + y;
}
}
let surf = BSplineSurface::interpolate_grid(&pts, 1, 1).expect("test: should succeed");
let pt = surf.evaluate(0.5, 0.5).expect("test: should succeed");
assert_abs_diff_eq!(pt[2], 1.0, epsilon = 0.05);
}
#[test]
fn test_interpolate_grid_passes_through_corners() {
let r = 3;
let s = 3;
let mut pts = Array3::<f64>::zeros((r + 1, s + 1, 3));
for i in 0..=r {
for j in 0..=s {
let x = i as f64 / r as f64;
let y = j as f64 / s as f64;
pts[[i, j, 0]] = x;
pts[[i, j, 1]] = y;
pts[[i, j, 2]] = (x * std::f64::consts::PI).sin();
}
}
let surf = BSplineSurface::interpolate_grid(&pts, 3, 3).expect("test: should succeed");
let (u_min, u_max) = surf.u_domain();
let (v_min, v_max) = surf.v_domain();
let pt_start = surf.evaluate(u_min, v_min).expect("test: should succeed");
assert_abs_diff_eq!(pt_start[2], 0.0, epsilon = 0.1);
let pt_end = surf.evaluate(u_max, v_max).expect("test: should succeed");
assert!(pt_end[2].is_finite());
}
#[test]
fn test_partial_derivative_u_bilinear() {
let surf = flat_bilinear();
let du = surf.partial_derivative_u(0.5, 0.5).expect("test: should succeed");
assert!(du[0].abs() > 0.5, "du_x should be significant");
assert!(du[2].abs() < 1e-8, "du_z should be zero on flat surface");
}
#[test]
fn test_partial_derivative_v_bilinear() {
let surf = flat_bilinear();
let dv = surf.partial_derivative_v(0.5, 0.5).expect("test: should succeed");
assert!(dv[1].abs() > 0.5, "dv_y should be significant");
assert!(dv[2].abs() < 1e-8, "dv_z should be zero on flat surface");
}
#[test]
fn test_make_bilinear_patch_tilted() {
let surf = make_bilinear_patch([
[0.0, 0.0, 0.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
[1.0, 1.0, 2.0],
])
.expect("test: should succeed");
let pt = surf.evaluate(0.5, 0.5).expect("test: should succeed");
assert_abs_diff_eq!(pt[2], 1.0, epsilon = 1e-8);
}
#[test]
fn test_accessors() {
let surf = flat_bilinear();
let (u_min, u_max) = surf.u_domain();
let (v_min, v_max) = surf.v_domain();
assert_abs_diff_eq!(u_min, 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(u_max, 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(v_min, 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(v_max, 1.0, epsilon = 1e-12);
}
#[test]
fn test_biquadratic_surface() {
let mut ctrl = Array3::<f64>::zeros((3, 3, 3));
for i in 0..3 {
for j in 0..3 {
ctrl[[i, j, 0]] = i as f64 * 0.5;
ctrl[[i, j, 1]] = j as f64 * 0.5;
ctrl[[i, j, 2]] = 0.0;
}
}
let u_knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let v_knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let surf = BSplineSurface::new(ctrl, u_knots, v_knots, 2, 2).expect("test: should succeed");
let pt = surf.evaluate(0.5, 0.5).expect("test: should succeed");
assert!(pt[0].is_finite() && pt[1].is_finite() && pt[2].is_finite());
}
}