use crate::nurbs::basis;
use crate::nurbs::power_basis::PowerBasis1D;
use crate::nurbs::surface::NurbsSurface;
use crate::vec::{Point3, Vec3};
pub struct SurfaceEvaluator<'a> {
surface: &'a NurbsSurface,
power_u: Option<PowerBasis1D>,
power_v: Option<PowerBasis1D>,
uniform_step_u: Option<f64>,
uniform_step_v: Option<f64>,
}
impl<'a> SurfaceEvaluator<'a> {
#[must_use]
pub fn new(surface: &'a NurbsSurface) -> Self {
let uniform_step_u = basis::uniform_knot_step(surface.knots_u(), surface.degree_u());
let uniform_step_v = basis::uniform_knot_step(surface.knots_v(), surface.degree_v());
Self {
surface,
power_u: None,
power_v: None,
uniform_step_u,
uniform_step_v,
}
}
fn ensure_power_basis(&mut self) {
if self.power_u.is_none() {
self.power_u = Some(PowerBasis1D::from_knots(
self.surface.knots_u(),
self.surface.degree_u(),
));
}
if self.power_v.is_none() {
self.power_v = Some(PowerBasis1D::from_knots(
self.surface.knots_v(),
self.surface.degree_v(),
));
}
}
fn find_span_u(&self, u: f64) -> usize {
let n = self.surface.control_points().len();
let pu = self.surface.degree_u();
if let Some(step) = self.uniform_step_u {
basis::find_span_uniform(n, pu, u, self.surface.knots_u(), step)
} else {
basis::find_span(n, pu, u, self.surface.knots_u())
}
}
fn find_span_v(&self, v: f64) -> usize {
let n = self.surface.control_points()[0].len();
let pv = self.surface.degree_v();
if let Some(step) = self.uniform_step_v {
basis::find_span_uniform(n, pv, v, self.surface.knots_v(), step)
} else {
basis::find_span(n, pv, v, self.surface.knots_v())
}
}
#[allow(clippy::many_single_char_names)]
pub fn point(&mut self, u: f64, v: f64) -> Point3 {
self.ensure_power_basis();
let pu = self.surface.degree_u();
let pv = self.surface.degree_v();
let span_u = self.find_span_u(u);
let span_v = self.find_span_v(v);
let mut nu = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
let mut nv = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
if let Some(ref pb_u) = self.power_u {
pb_u.horner(span_u, u, &mut nu[..=pu]);
}
if let Some(ref pb_v) = self.power_v {
pb_v.horner(span_v, v, &mut nv[..=pv]);
}
let cps = self.surface.control_points();
let ws = self.surface.weights();
let mut wx = 0.0;
let mut wy = 0.0;
let mut wz = 0.0;
let mut ww = 0.0;
for (i, &nu_i) in nu.iter().enumerate().take(pu + 1) {
let u_idx = span_u - pu + i;
let mut row_x = 0.0;
let mut row_y = 0.0;
let mut row_z = 0.0;
let mut row_w = 0.0;
for (j, &nv_j) in nv.iter().enumerate().take(pv + 1) {
let v_idx = span_v - pv + j;
let pt = &cps[u_idx][v_idx];
let w = ws[u_idx][v_idx];
let bw = nv_j * w;
row_x += bw * pt.x();
row_y += bw * pt.y();
row_z += bw * pt.z();
row_w += bw;
}
wx += nu_i * row_x;
wy += nu_i * row_y;
wz += nu_i * row_z;
ww += nu_i * row_w;
}
if ww == 0.0 {
Point3::new(wx, wy, wz)
} else {
Point3::new(wx / ww, wy / ww, wz / ww)
}
}
#[allow(clippy::many_single_char_names, clippy::too_many_lines)]
pub fn normal(&mut self, u: f64, v: f64) -> Vec3 {
self.ensure_power_basis();
let pu = self.surface.degree_u();
let pv = self.surface.degree_v();
let span_u = self.find_span_u(u);
let span_v = self.find_span_v(v);
let mut nu = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
let mut dnu = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
let mut nv = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
let mut dnv = [0.0_f64; basis::MAX_STACK_OUTPUT + 1];
if let Some(ref pb_u) = self.power_u {
pb_u.horner_with_derivs(span_u, u, &mut nu[..=pu], &mut dnu[..=pu]);
}
if let Some(ref pb_v) = self.power_v {
pb_v.horner_with_derivs(span_v, v, &mut nv[..=pv], &mut dnv[..=pv]);
}
let cps = self.surface.control_points();
let ws = self.surface.weights();
let mut s0 = [0.0_f64; 3]; let mut w0 = 0.0_f64; let mut su = [0.0_f64; 3]; let mut wu = 0.0_f64; let mut sv = [0.0_f64; 3]; let mut wv = 0.0_f64;
for (i, (&nu_i, &dnu_i)) in nu.iter().zip(dnu.iter()).enumerate().take(pu + 1) {
let u_idx = span_u - pu + i;
for (j, (&nv_j, &dnv_j)) in nv.iter().zip(dnv.iter()).enumerate().take(pv + 1) {
let v_idx = span_v - pv + j;
let pt = &cps[u_idx][v_idx];
let w = ws[u_idx][v_idx];
let px = pt.x();
let py = pt.y();
let pz = pt.z();
let nn_w = nu_i * nv_j * w;
s0[0] += nn_w * px;
s0[1] += nn_w * py;
s0[2] += nn_w * pz;
w0 += nn_w;
let dn_w = dnu_i * nv_j * w;
su[0] += dn_w * px;
su[1] += dn_w * py;
su[2] += dn_w * pz;
wu += dn_w;
let nd_w = nu_i * dnv_j * w;
sv[0] += nd_w * px;
sv[1] += nd_w * py;
sv[2] += nd_w * pz;
wv += nd_w;
}
}
if w0.abs() < f64::MIN_POSITIVE {
return self
.surface
.normal(u, v)
.unwrap_or_else(|_| Vec3::new(0.0, 0.0, 1.0));
}
let inv_w0 = 1.0 / w0;
let px = s0[0] * inv_w0;
let py = s0[1] * inv_w0;
let pz = s0[2] * inv_w0;
let du = Vec3::new(
(su[0] - wu * px) * inv_w0,
(su[1] - wu * py) * inv_w0,
(su[2] - wu * pz) * inv_w0,
);
let dv = Vec3::new(
(sv[0] - wv * px) * inv_w0,
(sv[1] - wv * py) * inv_w0,
(sv[2] - wv * pz) * inv_w0,
);
let cross = du.cross(dv);
if cross.length_squared() > 1e-30 {
cross
.normalize()
.unwrap_or_else(|_| Vec3::new(0.0, 0.0, 1.0))
} else {
self.surface
.normal(u, v)
.unwrap_or_else(|_| Vec3::new(0.0, 0.0, 1.0))
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::vec::Point3;
fn bicubic_surface() -> NurbsSurface {
let mut cps = Vec::new();
let mut ws = Vec::new();
for i in 0..4 {
let mut row = Vec::new();
let mut wrow = Vec::new();
#[allow(clippy::cast_precision_loss)]
for j in 0..4 {
row.push(Point3::new(
j as f64,
i as f64,
((i + j) as f64 * 0.5).sin(),
));
wrow.push(1.0);
}
cps.push(row);
ws.push(wrow);
}
NurbsSurface::new(
3,
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
cps,
ws,
)
.expect("valid bicubic surface")
}
#[test]
fn surface_evaluator_matches_evaluate() {
let surface = bicubic_surface();
let mut eval = surface.evaluator();
for i in 0..=10 {
for j in 0..=10 {
#[allow(clippy::cast_precision_loss)]
let u = i as f64 / 10.0;
#[allow(clippy::cast_precision_loss)]
let v = j as f64 / 10.0;
let expected = surface.evaluate(u, v);
let got = eval.point(u, v);
assert!(
(expected.x() - got.x()).abs() < 1e-10
&& (expected.y() - got.y()).abs() < 1e-10
&& (expected.z() - got.z()).abs() < 1e-10,
"mismatch at ({u},{v}): {expected:?} vs {got:?}"
);
}
}
}
#[test]
fn surface_evaluator_normal_matches() {
let surface = bicubic_surface();
let mut eval = surface.evaluator();
for i in 1..10 {
for j in 1..10 {
#[allow(clippy::cast_precision_loss)]
let u = i as f64 / 10.0;
#[allow(clippy::cast_precision_loss)]
let v = j as f64 / 10.0;
let expected = surface.normal(u, v).expect("non-degenerate");
let got = eval.normal(u, v);
let dot = expected.x() * got.x() + expected.y() * got.y() + expected.z() * got.z();
assert!(
(dot - 1.0).abs() < 1e-8,
"normal mismatch at ({u},{v}): dot={dot}"
);
}
}
}
}