use crate::curve_math::bspline as curve;
use crate::math::gauss_newton_2d;
use crate::projection::SurfaceProjection;
use crate::surface_math::bspline as math;
use crate::surfaces::ParametricSurface;
use crate::{Point3D, Tolerance, Vector3D};
pub use crate::curves::BSplineConstructionError;
#[derive(Debug, Clone, PartialEq)]
pub struct BSplineSurface {
u_degree: usize,
v_degree: usize,
u_periodic: bool,
v_periodic: bool,
poles: Vec<Vec<Point3D>>,
weights: Option<Vec<Vec<f64>>>,
u_knots: Vec<f64>,
u_mults: Vec<u32>,
v_knots: Vec<f64>,
v_mults: Vec<u32>,
u_flat: Vec<f64>,
v_flat: Vec<f64>,
flat_poles: Vec<f64>,
n_u: usize,
n_v: usize,
h: usize,
}
impl BSplineSurface {
#[allow(clippy::too_many_arguments)] pub fn new(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
Self::build(
u_degree,
v_degree,
poles,
None,
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
)
}
#[allow(clippy::too_many_arguments)] pub fn new_rational(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
weights: Vec<Vec<f64>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
Self::build(
u_degree,
v_degree,
poles,
Some(weights),
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
u_periodic,
v_periodic,
)
}
#[allow(clippy::too_many_arguments)] fn build(
u_degree: usize,
v_degree: usize,
poles: Vec<Vec<Point3D>>,
weights: Option<Vec<Vec<f64>>>,
u_knots: Vec<f64>,
u_multiplicities: Vec<u32>,
v_knots: Vec<f64>,
v_multiplicities: Vec<u32>,
u_periodic: bool,
v_periodic: bool,
) -> Result<BSplineSurface, BSplineConstructionError> {
let n_u = poles.len();
if n_u == 0 || poles[0].is_empty() {
return Err(BSplineConstructionError::PoleCountMismatch);
}
let n_v = poles[0].len();
if poles.iter().any(|row| row.len() != n_v) {
return Err(BSplineConstructionError::PoleCountMismatch);
}
if let Some(ws) = &weights {
if ws.len() != n_u || ws.iter().any(|row| row.len() != n_v) {
return Err(BSplineConstructionError::WeightCountMismatch);
}
if ws.iter().flatten().any(|&w| w <= 0.0) {
return Err(BSplineConstructionError::NonPositiveWeight);
}
}
curve::validate_direction(u_degree, n_u, &u_knots, &u_multiplicities, u_periodic)?;
curve::validate_direction(v_degree, n_v, &v_knots, &v_multiplicities, v_periodic)?;
let u_flat = curve::flat_knots(&u_knots, &u_multiplicities);
let v_flat = curve::flat_knots(&v_knots, &v_multiplicities);
let h = if weights.is_some() { 4 } else { 3 };
let flat_poles = pack_flat_poles(&poles, weights.as_ref(), h);
Ok(BSplineSurface {
u_degree,
v_degree,
u_periodic,
v_periodic,
poles,
weights,
u_knots,
u_mults: u_multiplicities,
v_knots,
v_mults: v_multiplicities,
u_flat,
v_flat,
flat_poles,
n_u,
n_v,
h,
})
}
pub fn u_degree(&self) -> usize {
self.u_degree
}
pub fn v_degree(&self) -> usize {
self.v_degree
}
pub fn is_u_periodic(&self) -> bool {
self.u_periodic
}
pub fn is_v_periodic(&self) -> bool {
self.v_periodic
}
pub fn is_rational(&self) -> bool {
self.weights.is_some()
}
pub fn eval_point(&self, u: f64, v: f64) -> Point3D {
math::surface_d0(
u,
v,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
)
}
pub fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point3D> {
uvs.iter().map(|&(u, v)| self.eval_point(u, v)).collect()
}
pub fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector3D {
match (du, dv) {
(0, 0) => panic!(
"eval_derivative: du + dv must be >= 1 (use eval_point for the (0, 0) order)"
),
(1, 0) | (0, 1) => {
let (_s, su, sv) = self.eval_surface_d1(u, v);
if du == 1 { su } else { sv }
}
_ => panic!(
"eval_derivative: order du={du}, dv={dv} is not supported (only first derivatives are supported)"
),
}
}
pub fn contains(&self, point: Point3D, tol: Tolerance) -> bool {
self.project_point(point, tol).distance <= tol.confusion
}
pub fn extrema(&self, point: Point3D, tol: Tolerance) -> Vec<SurfaceProjection> {
use crate::projection::snap_distance;
let (u0, u1) = ParametricSurface::u_bounds(self);
let (v0, v1) = ParametricSurface::v_bounds(self);
let clamp_u = |u: f64| {
if self.is_u_periodic() {
u
} else {
u.clamp(u0, u1)
}
};
let clamp_v = |v: f64| {
if self.is_v_periodic() {
v
} else {
v.clamp(v0, v1)
}
};
const NU: usize = 16;
const NV: usize = 16;
const KEEP: usize = 12;
let mut samples: Vec<(f64, f64, f64)> = Vec::with_capacity((NU + 1) * (NV + 1));
for j in 0..=NV {
for i in 0..=NU {
let u = u0 + (u1 - u0) * i as f64 / NU as f64;
let v = v0 + (v1 - v0) * j as f64 / NV as f64;
samples.push((u, v, self.eval_point(u, v).distance(point)));
}
}
samples.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
let mut params: Vec<(f64, f64)> = Vec::new();
for (u, v, _) in samples.into_iter().take(KEEP) {
let p = self.eval_point(u, v);
if params
.iter()
.any(|&(su, sv)| self.eval_point(su, sv).distance(p) <= tol.confusion)
{
continue;
}
let residual = |u: f64, v: f64| {
let q = self.eval_point(clamp_u(u), clamp_v(v)) - point;
[q.x, q.y, q.z]
};
let jac_u = |u: f64, v: f64| {
let d = self.eval_derivative(clamp_u(u), clamp_v(v), 1, 0);
[d.x, d.y, d.z]
};
let jac_v = |u: f64, v: f64| {
let d = self.eval_derivative(clamp_u(u), clamp_v(v), 0, 1);
[d.x, d.y, d.z]
};
if let Some((ru, rv)) = gauss_newton_2d(residual, jac_u, jac_v, u, v, tol.confusion, 50)
{
params.push((clamp_u(ru), clamp_v(rv)));
}
}
if params.is_empty() {
params.push((u0, v0));
}
let mut out: Vec<SurfaceProjection> = Vec::new();
for (u, v) in params {
let p = self.eval_point(u, v);
if out
.iter()
.any(|e: &SurfaceProjection| self.eval_point(e.u, e.v).distance(p) <= tol.confusion)
{
continue;
}
let distance = p.distance(point);
out.push(SurfaceProjection {
u,
v,
distance: snap_distance(distance, tol.confusion),
});
}
out.sort_by(|x, y| x.distance.partial_cmp(&y.distance).unwrap());
out
}
pub fn project_point(&self, point: Point3D, tol: Tolerance) -> SurfaceProjection {
self.extrema(point, tol)
.into_iter()
.next()
.expect("seeding guarantees a non-empty candidate list")
}
pub fn project_points(&self, points: &[Point3D], tol: Tolerance) -> Vec<SurfaceProjection> {
points.iter().map(|&p| self.project_point(p, tol)).collect()
}
fn eval_surface_d1(&self, u: f64, v: f64) -> (Point3D, Vector3D, Vector3D) {
math::surface_d1(
u,
v,
self.u_degree,
self.v_degree,
self.u_periodic,
self.v_periodic,
&self.u_flat,
&self.v_flat,
&self.flat_poles,
self.n_u,
self.n_v,
self.h,
)
}
fn u_param_range(&self) -> (f64, f64) {
let sum_mults: u32 = self.u_mults.iter().sum();
curve::param_range(
&self.u_flat,
self.u_degree,
self.u_periodic,
sum_mults as usize,
)
}
fn v_param_range(&self) -> (f64, f64) {
let sum_mults: u32 = self.v_mults.iter().sum();
curve::param_range(
&self.v_flat,
self.v_degree,
self.v_periodic,
sum_mults as usize,
)
}
}
impl ParametricSurface for BSplineSurface {
fn eval_point(&self, u: f64, v: f64) -> Point3D {
BSplineSurface::eval_point(self, u, v)
}
fn project_point(&self, point: Point3D, tol: Tolerance) -> SurfaceProjection {
BSplineSurface::project_point(self, point, tol)
}
fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector3D {
BSplineSurface::eval_derivative(self, u, v, du, dv)
}
fn u_bounds(&self) -> (f64, f64) {
self.u_param_range()
}
fn v_bounds(&self) -> (f64, f64) {
self.v_param_range()
}
fn u_period(&self) -> Option<f64> {
if self.u_periodic {
let (first, last) = self.u_param_range();
Some(last - first)
} else {
None
}
}
fn v_period(&self) -> Option<f64> {
if self.v_periodic {
let (first, last) = self.v_param_range();
Some(last - first)
} else {
None
}
}
fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point3D> {
BSplineSurface::eval_points(self, uvs)
}
}
fn pack_flat_poles(poles: &[Vec<Point3D>], weights: Option<&Vec<Vec<f64>>>, h: usize) -> Vec<f64> {
let mut out = Vec::with_capacity(poles.len() * poles[0].len() * h);
for (iu, row) in poles.iter().enumerate() {
for (iv, p) in row.iter().enumerate() {
match weights {
Some(ws) => {
let w = ws[iu][iv];
out.extend_from_slice(&[p.x * w, p.y * w, p.z * w, w]);
}
None => out.extend_from_slice(&[p.x, p.y, p.z]),
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn bilinear() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0), Point3D::new(1.0, 1.0, 1.0)],
];
BSplineSurface::new(
1,
1,
poles,
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_accessors() {
let s = bilinear();
assert_eq!(s.u_degree(), 1);
assert_eq!(s.v_degree(), 1);
assert!(!s.is_u_periodic());
assert!(!s.is_v_periodic());
assert!(!s.is_rational());
}
#[test]
fn test_bilinear_point_by_hand() {
let s = bilinear();
assert_eq!(s.eval_point(0.5, 0.5), Point3D::new(0.5, 0.5, 0.25));
assert_eq!(s.eval_point(0.0, 0.0), Point3D::new(0.0, 0.0, 0.0));
assert_eq!(s.eval_point(1.0, 1.0), Point3D::new(1.0, 1.0, 1.0));
assert_eq!(s.eval_point(0.3, 0.7), Point3D::new(0.3, 0.7, 0.3 * 0.7));
}
#[test]
fn test_bilinear_derivatives_by_hand() {
let s = bilinear();
assert_eq!(
s.eval_derivative(0.3, 0.7, 1, 0),
Vector3D::new(1.0, 0.0, 0.7)
);
assert_eq!(
s.eval_derivative(0.3, 0.7, 0, 1),
Vector3D::new(0.0, 1.0, 0.3)
);
}
#[test]
fn test_eval_points_matches_mapped_eval_point() {
let s = bilinear();
let uvs = [(0.0, 0.0), (0.2, 0.4), (0.5, 0.5), (1.0, 1.0)];
let expected: Vec<Point3D> = uvs.iter().map(|&(u, v)| s.eval_point(u, v)).collect();
assert_eq!(s.eval_points(&uvs), expected);
}
#[test]
#[should_panic(expected = "du + dv must be >= 1")]
fn test_eval_derivative_zero_order_panics() {
bilinear().eval_derivative(0.5, 0.5, 0, 0);
}
#[test]
#[should_panic(expected = "only first derivatives are supported")]
fn test_eval_derivative_second_order_uu_panics() {
bilinear().eval_derivative(0.5, 0.5, 2, 0);
}
#[test]
#[should_panic(expected = "only first derivatives are supported")]
fn test_eval_derivative_mixed_second_order_panics() {
bilinear().eval_derivative(0.5, 0.5, 1, 1);
}
#[test]
fn test_bounds_clamped() {
let s = bilinear();
assert_eq!(ParametricSurface::u_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::v_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::u_period(&s), None);
assert_eq!(ParametricSurface::v_period(&s), None);
}
fn ok_grid() -> Vec<Vec<Point3D>> {
vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0), Point3D::new(1.0, 1.0, 1.0)],
]
}
#[test]
fn test_empty_grid_is_pole_count_mismatch() {
let err = BSplineSurface::new(
1,
1,
vec![],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_ragged_grid_is_pole_count_mismatch() {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 1.0, 0.0)],
vec![Point3D::new(1.0, 0.0, 0.0)], ];
let err = BSplineSurface::new(
1,
1,
poles,
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_invalid_u_degree() {
let err = BSplineSurface::new(
0,
1,
ok_grid(),
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::InvalidDegree);
}
#[test]
fn test_u_pole_count_mismatch_from_knots() {
let err = BSplineSurface::new(
1,
1,
ok_grid(),
vec![0.0, 1.0, 2.0],
vec![2, 1, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::PoleCountMismatch);
}
#[test]
fn test_v_knots_not_increasing() {
let err = BSplineSurface::new(
1,
1,
ok_grid(),
vec![0.0, 1.0],
vec![2, 2],
vec![1.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::KnotsNotIncreasing);
}
#[test]
fn test_weight_count_mismatch_ragged() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0], vec![1.0]], vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::WeightCountMismatch);
}
#[test]
fn test_weight_count_mismatch_wrong_rows() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0]], vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::WeightCountMismatch);
}
#[test]
fn test_non_positive_weight() {
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 0.0], vec![1.0, 1.0]],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::NonPositiveWeight);
let err = BSplineSurface::new_rational(
1,
1,
ok_grid(),
vec![vec![1.0, 1.0], vec![-2.0, 1.0]],
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap_err();
assert_eq!(err, BSplineConstructionError::NonPositiveWeight);
}
fn periodic_tube() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(2.0, 0.0, 0.0), Point3D::new(2.2, 0.0, 3.0)],
vec![
Point3D::new(1.0000000000000002, 1.7320508075688772, 0.0),
Point3D::new(1.1000000000000003, 1.905255888325765, 3.0),
],
vec![
Point3D::new(-0.9999999999999996, 1.7320508075688774, 0.0),
Point3D::new(-1.0999999999999996, 1.9052558883257653, 3.0),
],
vec![
Point3D::new(-2.0, 2.4492935982947064e-16, 0.0),
Point3D::new(-2.2, 2.6942229581241775e-16, 3.0),
],
vec![
Point3D::new(-1.0000000000000009, -1.7320508075688767, 0.0),
Point3D::new(-1.100000000000001, -1.9052558883257646, 3.0),
],
vec![
Point3D::new(1.0000000000000002, -1.7320508075688772, 0.0),
Point3D::new(1.1000000000000003, -1.905255888325765, 3.0),
],
];
BSplineSurface::new(
2,
1,
poles,
vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
vec![1, 1, 1, 1, 1, 1, 1],
vec![0.0, 1.0],
vec![2, 2],
true,
false,
)
.unwrap()
}
#[test]
fn test_periodic_bounds_and_period() {
let s = periodic_tube();
assert_eq!(ParametricSurface::u_bounds(&s), (0.0, 6.0));
assert_eq!(ParametricSurface::v_bounds(&s), (0.0, 1.0));
assert_eq!(ParametricSurface::u_period(&s), Some(6.0));
assert_eq!(ParametricSurface::v_period(&s), None);
}
#[test]
fn test_periodic_seam_matches_across_period() {
let s = periodic_tube();
for v in [0.0, 0.3, 0.7, 1.0] {
let p0 = s.eval_point(0.0, v);
let p1 = s.eval_point(6.0, v);
assert!((p0.x - p1.x).abs() < 1e-9, "x seam v={v}");
assert!((p0.y - p1.y).abs() < 1e-9, "y seam v={v}");
assert!((p0.z - p1.z).abs() < 1e-9, "z seam v={v}");
let d0 = s.eval_derivative(0.0, v, 1, 0);
let d1 = s.eval_derivative(6.0, v, 1, 0);
assert!((d0.x - d1.x).abs() < 1e-9, "du.x seam v={v}");
assert!((d0.y - d1.y).abs() < 1e-9, "du.y seam v={v}");
assert!((d0.z - d1.z).abs() < 1e-9, "du.z seam v={v}");
}
}
#[test]
fn test_periodic_out_of_window_u_matches_golden() {
let s = periodic_tube();
let p = s.eval_point(-0.8, 0.6);
assert!((p.x - 1.7596000000000003).abs() < 1e-7);
assert!((p.y - -0.5507921568069025).abs() < 1e-7);
assert!((p.z - 1.7999999999999998).abs() < 1e-7);
}
fn bilinear_patch() -> BSplineSurface {
let poles = vec![
vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 2.0, 0.0)],
vec![Point3D::new(2.0, 0.0, 0.0), Point3D::new(2.0, 2.0, 0.0)],
];
BSplineSurface::new(
1,
1,
poles,
vec![0.0, 1.0],
vec![2, 2],
vec![0.0, 1.0],
vec![2, 2],
false,
false,
)
.unwrap()
}
#[test]
fn test_bspline_surface_project_point_planar() {
let s = bilinear_patch();
let tol = Tolerance::DEFAULT;
let proj = s.project_point(Point3D::new(1.0, 1.0, 1.0), tol);
assert!((proj.u - 0.5).abs() < 1e-9);
assert!((proj.v - 0.5).abs() < 1e-9);
assert!((proj.distance - 1.0).abs() < 1e-9);
let edge = s.project_point(Point3D::new(3.0, 1.0, 0.0), tol);
assert!((edge.u - 1.0).abs() < 1e-9);
assert!((edge.v - 0.5).abs() < 1e-9);
assert!((edge.distance - 1.0).abs() < 1e-9);
}
#[test]
fn test_bspline_surface_contains_planar() {
let s = bilinear_patch();
let tol = Tolerance::DEFAULT;
assert!(s.contains(Point3D::new(1.0, 1.0, 0.0), tol));
assert!(s.contains(s.eval_point(0.25, 0.75), tol));
assert!(!s.contains(Point3D::new(1.0, 1.0, 1.0), tol));
}
#[test]
fn test_bspline_surface_project_point_tube() {
let s = periodic_tube();
let tol = Tolerance::DEFAULT;
let on = s.eval_point(0.5, 0.5);
assert_eq!(s.project_point(on, tol).distance, 0.0);
assert!(s.contains(on, tol));
let query = Point3D::new(0.0, 0.0, 5.0);
let proj = s.project_point(query, tol);
let (u0, u1) = ParametricSurface::u_bounds(&s);
let (v0, v1) = ParametricSurface::v_bounds(&s);
let mut best = f64::INFINITY;
for j in 0..=60 {
for i in 0..=60 {
let d = s
.eval_point(
u0 + (u1 - u0) * i as f64 / 60.0,
v0 + (v1 - v0) * j as f64 / 60.0,
)
.distance(query);
best = best.min(d);
}
}
assert!(proj.distance <= best + 1e-9);
assert!(best - proj.distance < 1e-2);
}
}