use crate::geometry::Point;
use std::fmt;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NURBS {
control_points: Vec<Point>,
weights: Vec<f64>,
knots: Vec<f64>,
degree: usize,
}
impl NURBS {
#[inline]
pub fn new(control_points: Vec<Point>, weights: Vec<f64>, knots: Vec<f64>, degree: usize) -> Self {
Self {
control_points,
weights,
knots,
degree,
}
}
#[inline]
pub fn from_points(points: Vec<Point>, degree: usize) -> Self {
let n = points.len() - 1;
let weights = vec![1.0; points.len()];
let mut knots = Vec::with_capacity(n + degree + 2);
for _ in 0..=degree {
knots.push(0.0);
}
for i in 1..=n - degree {
let t = i as f64 / (n - degree + 1) as f64;
knots.push(t);
}
for _ in 0..=degree {
knots.push(1.0);
}
Self {
control_points: points,
weights,
knots,
degree,
}
}
#[inline]
pub fn with_circle(center: Point, radius: f64, segments: usize) -> Self {
let mut points = Vec::with_capacity(segments);
let mut weights = Vec::with_capacity(segments);
for i in 0..segments {
let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
points.push(Point::new(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
center.z,
));
weights.push(1.0);
}
Self::from_points(points, 2)
}
#[inline]
pub fn control_points(&self) -> &[Point] {
&self.control_points
}
#[inline]
pub fn weights(&self) -> &[f64] {
&self.weights
}
#[inline]
pub fn knots(&self) -> &[f64] {
&self.knots
}
#[inline]
pub fn degree(&self) -> usize {
self.degree
}
#[inline]
pub fn is_rational(&self) -> bool {
self.weights.iter().any(|&w| (w - 1.0).abs() > 1e-10)
}
#[inline]
pub fn point_at(&self, t: f64) -> Point {
self.evaluate_point(t.clamp(0.0, 1.0))
}
fn evaluate_point(&self, t: f64) -> Point {
let basis = self.compute_rational_basis(t);
let mut wx = 0.0;
let mut wy = 0.0;
let mut wz = 0.0;
let mut w = 0.0;
for (i, &b) in basis.iter().enumerate() {
if b > 0.0 {
wx += self.control_points[i].x * self.weights[i] * b;
wy += self.control_points[i].y * self.weights[i] * b;
wz += self.control_points[i].z * self.weights[i] * b;
w += self.weights[i] * b;
}
}
if w.abs() < 1e-10 {
Point::origin()
} else {
Point::new(wx / w, wy / w, wz / w)
}
}
fn compute_rational_basis(&self, t: f64) -> Vec<f64> {
let n = self.control_points.len();
let mut basis = vec![0.0; n];
let mut ndu = vec![vec![0.0; self.degree + 1]; n];
ndu[0][0] = if t >= self.knots[0] && t < self.knots[1] { 1.0 } else { 0.0 };
for j in 1..=self.degree {
for i in 0..=n - j - 1 {
let denom1 = self.knots[i + j - 1] - self.knots[i];
let denom2 = self.knots[i + j] - self.knots[i + 1];
let left = if denom1 != 0.0 {
((t - self.knots[i]) / denom1) * ndu[i][j - 1]
} else {
0.0
};
let right = if denom2 != 0.0 {
((self.knots[i + j] - t) / denom2) * ndu[i + 1][j - 1]
} else {
0.0
};
ndu[i][j] = left + right;
}
}
for i in 0..n {
basis[i] = ndu[i][self.degree];
}
basis
}
}
impl fmt::Display for NURBS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"NURBS(degree: {}, control_points: {}, rational: {})",
self.degree,
self.control_points.len(),
self.is_rational()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nurbs_creation() {
let points = vec![
Point::new(0.0, 0.0, 0.0),
Point::new(1.0, 1.0, 0.0),
Point::new(2.0, 1.0, 0.0),
Point::new(3.0, 0.0, 0.0),
];
let nurbs = NURBS::from_points(points, 2);
assert_eq!(nurbs.degree(), 2);
}
#[test]
fn test_nurbs_is_rational() {
let points = vec![
Point::new(0.0, 0.0, 0.0),
Point::new(1.0, 1.0, 0.0),
];
let nurbs = NURBS::from_points(points, 1);
assert!(!nurbs.is_rational());
}
#[test]
fn test_nurbs_point_at() {
let points = vec![
Point::new(0.0, 0.0, 0.0),
Point::new(1.0, 1.0, 0.0),
Point::new(2.0, 1.0, 0.0),
Point::new(3.0, 0.0, 0.0),
];
let nurbs = NURBS::from_points(points, 2);
let start = nurbs.point_at(0.0);
assert!((start.x - 0.0).abs() < 1e-10);
}
}