use crate::MathError;
use crate::nurbs::basis;
use crate::nurbs::curve::NurbsCurve;
use crate::nurbs::surface::NurbsSurface;
use crate::vec::Point3;
const KNOT_EPS: f64 = 1e-15;
pub fn curve_knot_insert(curve: &NurbsCurve, u: f64, r: usize) -> Result<NurbsCurve, MathError> {
if r == 0 {
return Ok(curve.clone());
}
let p = curve.degree();
let knots = curve.knots();
let cps = curve.control_points();
let ws = curve.weights();
let s = knots
.iter()
.filter(|&&kv| (kv - u).abs() < KNOT_EPS)
.count();
let r = r.min(p.saturating_sub(s)); if r == 0 {
return Ok(curve.clone());
}
let qw: Vec<[f64; 4]> = cps
.iter()
.zip(ws.iter())
.map(|(pt, &w)| [pt.x() * w, pt.y() * w, pt.z() * w, w])
.collect();
let mut current_qw = qw;
let mut current_knots: Vec<f64> = knots.to_vec();
for _ins in 0..r {
let cn = current_qw.len();
let ck = basis::find_span(cn, p, u, ¤t_knots);
let mut new_qw = Vec::with_capacity(cn + 1);
new_qw.extend_from_slice(¤t_qw[..=ck.saturating_sub(p)]);
for i in (ck - p + 1)..=ck {
let denom = current_knots[i + p] - current_knots[i];
let alpha = if denom.abs() < KNOT_EPS {
0.0
} else {
(u - current_knots[i]) / denom
};
let prev = current_qw[i - 1];
let curr = current_qw[i];
new_qw.push([
alpha * curr[0] + (1.0 - alpha) * prev[0],
alpha * curr[1] + (1.0 - alpha) * prev[1],
alpha * curr[2] + (1.0 - alpha) * prev[2],
alpha * curr[3] + (1.0 - alpha) * prev[3],
]);
}
new_qw.extend_from_slice(¤t_qw[ck..cn]);
let mut new_kv = Vec::with_capacity(current_knots.len() + 1);
new_kv.extend_from_slice(¤t_knots[..=ck]);
new_kv.push(u);
new_kv.extend_from_slice(¤t_knots[ck + 1..]);
current_qw = new_qw;
current_knots = new_kv;
}
let new_cps: Vec<Point3> = current_qw
.iter()
.map(|h| {
if h[3] == 0.0 {
Point3::new(h[0], h[1], h[2])
} else {
Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3])
}
})
.collect();
let new_ws: Vec<f64> = current_qw.iter().map(|h| h[3]).collect();
NurbsCurve::new(p, current_knots, new_cps, new_ws)
}
#[allow(clippy::similar_names)]
pub fn surface_knot_insert_u(
surface: &NurbsSurface,
u: f64,
r: usize,
) -> Result<NurbsSurface, MathError> {
let pu = surface.degree_u();
let pv = surface.degree_v();
let n_cols = surface.control_points()[0].len();
let mut new_rows: Option<Vec<Vec<Point3>>> = None;
let mut new_wrows: Option<Vec<Vec<f64>>> = None;
let mut new_knots_u = Vec::new();
for col in 0..n_cols {
let col_cps: Vec<Point3> = surface
.control_points()
.iter()
.map(|row| row[col])
.collect();
let col_ws: Vec<f64> = surface.weights().iter().map(|row| row[col]).collect();
let col_curve = NurbsCurve::new(pu, surface.knots_u().to_vec(), col_cps, col_ws)?;
let inserted = curve_knot_insert(&col_curve, u, r)?;
if new_knots_u.is_empty() {
new_knots_u = inserted.knots().to_vec();
}
let n_new_rows = inserted.control_points().len();
let rows = new_rows.get_or_insert_with(|| vec![Vec::with_capacity(n_cols); n_new_rows]);
let wrows = new_wrows.get_or_insert_with(|| vec![Vec::with_capacity(n_cols); n_new_rows]);
for (i, (pt, &w)) in inserted
.control_points()
.iter()
.zip(inserted.weights().iter())
.enumerate()
{
rows[i].push(*pt);
wrows[i].push(w);
}
}
let rows = new_rows.ok_or(MathError::EmptyInput)?;
let wrows = new_wrows.ok_or(MathError::EmptyInput)?;
NurbsSurface::new(pu, pv, new_knots_u, surface.knots_v().to_vec(), rows, wrows)
}
#[allow(clippy::similar_names)]
pub fn surface_knot_insert_v(
surface: &NurbsSurface,
v: f64,
r: usize,
) -> Result<NurbsSurface, MathError> {
let pu = surface.degree_u();
let pv = surface.degree_v();
let mut new_rows = Vec::with_capacity(surface.control_points().len());
let mut new_wrows = Vec::with_capacity(surface.weights().len());
let mut new_knots_v = Vec::new();
for (row_cps, row_ws) in surface
.control_points()
.iter()
.zip(surface.weights().iter())
{
let row_curve = NurbsCurve::new(
pv,
surface.knots_v().to_vec(),
row_cps.clone(),
row_ws.clone(),
)?;
let inserted = curve_knot_insert(&row_curve, v, r)?;
if new_knots_v.is_empty() {
new_knots_v = inserted.knots().to_vec();
}
new_rows.push(inserted.control_points().to_vec());
new_wrows.push(inserted.weights().to_vec());
}
NurbsSurface::new(
pu,
pv,
surface.knots_u().to_vec(),
new_knots_v,
new_rows,
new_wrows,
)
}
#[allow(
clippy::similar_names,
clippy::too_many_lines,
clippy::many_single_char_names,
clippy::suboptimal_flops
)]
pub fn curve_knot_remove(
curve: &NurbsCurve,
u: f64,
tolerance: f64,
) -> Result<NurbsCurve, MathError> {
let p = curve.degree();
let knots = curve.knots();
let cps = curve.control_points();
let ws = curve.weights();
let n = cps.len();
let mut r_last: Option<usize> = None;
for (i, &kv) in knots.iter().enumerate() {
if (kv - u).abs() < KNOT_EPS {
r_last = Some(i);
}
}
let Some(r) = r_last else {
return Ok(curve.clone());
};
let pw: Vec<[f64; 4]> = cps
.iter()
.zip(ws.iter())
.map(|(pt, &w)| [pt.x() * w, pt.y() * w, pt.z() * w, w])
.collect();
let mut removed_knots: Vec<f64> = Vec::with_capacity(knots.len() - 1);
{
let mut skipped = false;
for (idx, &kv) in knots.iter().enumerate() {
if !skipped && idx == r {
skipped = true;
continue;
}
removed_knots.push(kv);
}
}
let n_old = n - 1; let k = basis::find_span(n_old, p, u, &removed_knots);
let num_affected = p; let mut left_pts = Vec::with_capacity(num_affected);
let mut right_pts = Vec::with_capacity(num_affected);
{
let mut prev = pw[k - p]; for idx in (k - p + 1)..=(k) {
let denom = removed_knots[idx + p] - removed_knots[idx];
let alpha = if denom.abs() < KNOT_EPS {
0.0
} else {
(u - removed_knots[idx]) / denom
};
let qi = pw[idx]; let old_i = if alpha.abs() < KNOT_EPS {
qi
} else {
[
(qi[0] - (1.0 - alpha) * prev[0]) / alpha,
(qi[1] - (1.0 - alpha) * prev[1]) / alpha,
(qi[2] - (1.0 - alpha) * prev[2]) / alpha,
(qi[3] - (1.0 - alpha) * prev[3]) / alpha,
]
};
left_pts.push(old_i);
prev = old_i;
}
}
{
let mut next = pw[k + 1]; for idx in ((k - p + 1)..=(k)).rev() {
let denom = removed_knots[idx + p] - removed_knots[idx];
let alpha = if denom.abs() < KNOT_EPS {
0.0
} else {
(u - removed_knots[idx]) / denom
};
let qi = pw[idx]; let old_im1 = if (1.0 - alpha).abs() < KNOT_EPS {
qi
} else {
[
(qi[0] - alpha * next[0]) / (1.0 - alpha),
(qi[1] - alpha * next[1]) / (1.0 - alpha),
(qi[2] - alpha * next[2]) / (1.0 - alpha),
(qi[3] - alpha * next[3]) / (1.0 - alpha),
]
};
right_pts.push(old_im1);
next = old_im1;
}
}
right_pts.reverse();
for idx in 0..p.saturating_sub(1) {
let lp = left_pts[idx];
let rp = right_pts[idx + 1];
let dist = if lp[3].abs() > KNOT_EPS && rp[3].abs() > KNOT_EPS {
let dx = lp[0] / lp[3] - rp[0] / rp[3];
let dy = lp[1] / lp[3] - rp[1] / rp[3];
let dz = lp[2] / lp[3] - rp[2] / rp[3];
(dx * dx + dy * dy + dz * dz).sqrt()
} else {
let dx = lp[0] - rp[0];
let dy = lp[1] - rp[1];
let dz = lp[2] - rp[2];
let dw = lp[3] - rp[3];
(dx * dx + dy * dy + dz * dz + dw * dw).sqrt()
};
if dist > tolerance {
return Err(MathError::ConvergenceFailure { iterations: 0 });
}
}
let mut new_pw: Vec<[f64; 4]> = Vec::with_capacity(n_old);
new_pw.extend_from_slice(&pw[..=k - p]);
for idx in 0..(p - 1) {
let lp = left_pts[idx];
let rp = right_pts[idx + 1];
new_pw.push([
(lp[0] + rp[0]) * 0.5,
(lp[1] + rp[1]) * 0.5,
(lp[2] + rp[2]) * 0.5,
(lp[3] + rp[3]) * 0.5,
]);
}
new_pw.extend_from_slice(&pw[k + 1..]);
let new_knots = removed_knots;
let new_cps: Vec<Point3> = new_pw
.iter()
.map(|h| {
if h[3] == 0.0 {
Point3::new(h[0], h[1], h[2])
} else {
Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3])
}
})
.collect();
let new_ws: Vec<f64> = new_pw.iter().map(|h| h[3]).collect();
NurbsCurve::new(p, new_knots, new_cps, new_ws)
}
pub fn curve_knot_refine(
curve: &NurbsCurve,
knots_to_insert: &[f64],
) -> Result<NurbsCurve, MathError> {
if knots_to_insert.is_empty() {
return Ok(curve.clone());
}
let mut result = curve.clone();
for &u in knots_to_insert {
result = curve_knot_insert(&result, u, 1)?;
}
Ok(result)
}
pub fn curve_split(curve: &NurbsCurve, u: f64) -> Result<(NurbsCurve, NurbsCurve), MathError> {
let p = curve.degree();
let refined = curve_knot_insert(curve, u, p)?;
let knots = refined.knots();
let cps = refined.control_points();
let ws = refined.weights();
let first_u = knots
.iter()
.position(|&k| (k - u).abs() < KNOT_EPS)
.ok_or(MathError::EmptyInput)?;
let mut last_u = first_u;
while last_u + 1 < knots.len() && (knots[last_u + 1] - u).abs() < KNOT_EPS {
last_u += 1;
}
let split_cp = last_u - p;
let mult = last_u - first_u + 1;
let mut left_knots: Vec<f64> = knots[..=last_u].to_vec();
for _ in 0..(p + 1 - mult) {
left_knots.push(u);
}
let left_cps: Vec<Point3> = cps[..=split_cp].to_vec();
let left_ws: Vec<f64> = ws[..=split_cp].to_vec();
let mut right_knots: Vec<f64> = Vec::new();
for _ in 0..(p + 1 - mult) {
right_knots.push(u);
}
right_knots.extend_from_slice(&knots[first_u..]);
let right_cps: Vec<Point3> = cps[split_cp..].to_vec();
let right_ws: Vec<f64> = ws[split_cp..].to_vec();
let left = NurbsCurve::new(p, left_knots, left_cps, left_ws)?;
let right = NurbsCurve::new(p, right_knots, right_cps, right_ws)?;
Ok((left, right))
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::cast_lossless, clippy::suboptimal_flops)]
mod tests {
use super::*;
fn cubic_bezier() -> NurbsCurve {
NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
Point3::new(3.0, 2.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
],
vec![1.0, 1.0, 1.0, 1.0],
)
.expect("valid")
}
fn multi_span_curve() -> NurbsCurve {
NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
Point3::new(2.0, 2.0, 0.0),
Point3::new(3.0, 1.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
],
vec![1.0, 1.0, 1.0, 1.0, 1.0],
)
.expect("valid")
}
#[test]
fn knot_insert_preserves_shape() {
let c = multi_span_curve();
let inserted = curve_knot_insert(&c, 0.25, 1).expect("valid");
assert_eq!(
inserted.control_points().len(),
c.control_points().len() + 1
);
assert_eq!(inserted.knots().len(), c.knots().len() + 1);
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = inserted.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-12
&& (p1.y() - p2.y()).abs() < 1e-12
&& (p1.z() - p2.z()).abs() < 1e-12,
"shape mismatch at u={u}"
);
}
}
#[test]
fn knot_insert_multiple() {
let c = multi_span_curve();
let inserted = curve_knot_insert(&c, 0.25, 2).expect("valid");
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = inserted.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-11
&& (p1.y() - p2.y()).abs() < 1e-11
&& (p1.z() - p2.z()).abs() < 1e-11,
"shape mismatch at u={u}"
);
}
}
#[test]
fn knot_refine_preserves_shape() {
let c = cubic_bezier();
let refined = curve_knot_refine(&c, &[0.25, 0.5, 0.75]).expect("valid");
for i in 0..=20 {
let u = i as f64 / 20.0;
let p1 = c.evaluate(u);
let p2 = refined.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-11 && (p1.y() - p2.y()).abs() < 1e-11,
"shape mismatch at u={u}"
);
}
}
#[test]
fn curve_split_basic() {
let c = cubic_bezier();
let (left, right) = curve_split(&c, 0.5).expect("valid split");
let pl = left.evaluate(0.0);
let pc_start = c.evaluate(0.0);
assert!((pl.x() - pc_start.x()).abs() < 1e-12);
let pr = right.evaluate(1.0);
let pc_end = c.evaluate(1.0);
assert!((pr.x() - pc_end.x()).abs() < 1e-12);
}
#[test]
fn knot_insert_zero_times_returns_clone() {
let c = cubic_bezier();
let inserted = curve_knot_insert(&c, 0.5, 0).expect("valid");
assert_eq!(inserted.knots().len(), c.knots().len());
}
#[test]
fn surface_knot_insert_u_preserves_shape() {
let surf = NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.expect("valid surface");
let refined = surface_knot_insert_u(&surf, 0.5, 1).expect("valid insert");
for i in 0..=4 {
for j in 0..=4 {
let u = i as f64 / 4.0;
let v = j as f64 / 4.0;
let p1 = surf.evaluate(u, v);
let p2 = refined.evaluate(u, v);
assert!(
(p1.x() - p2.x()).abs() < 1e-10
&& (p1.y() - p2.y()).abs() < 1e-10
&& (p1.z() - p2.z()).abs() < 1e-10,
"shape mismatch at ({u}, {v})"
);
}
}
}
#[test]
fn surface_knot_insert_v_preserves_shape() {
let surf = NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
vec![Point3::new(0.0, 1.0, 0.5), Point3::new(1.0, 1.0, 0.5)],
],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.expect("valid surface");
let refined = surface_knot_insert_v(&surf, 0.5, 1).expect("valid insert");
for i in 0..=4 {
for j in 0..=4 {
let u = i as f64 / 4.0;
let v = j as f64 / 4.0;
let p1 = surf.evaluate(u, v);
let p2 = refined.evaluate(u, v);
assert!(
(p1.x() - p2.x()).abs() < 1e-10
&& (p1.y() - p2.y()).abs() < 1e-10
&& (p1.z() - p2.z()).abs() < 1e-10,
"shape mismatch at ({u}, {v})"
);
}
}
}
#[test]
fn curve_split_preserves_endpoints() {
let c = cubic_bezier();
let (left, right) = curve_split(&c, 0.5).expect("valid split");
let l_start = left.evaluate(0.0);
let c_start = c.evaluate(0.0);
assert!(
(l_start.x() - c_start.x()).abs() < 1e-10,
"left start should match original start"
);
let r_end = right.evaluate(1.0);
let c_end = c.evaluate(1.0);
assert!(
(r_end.x() - c_end.x()).abs() < 1e-10,
"right end should match original end"
);
}
#[test]
fn knot_refine_empty_list() {
let c = cubic_bezier();
let refined = curve_knot_refine(&c, &[]).expect("valid");
assert_eq!(refined.knots().len(), c.knots().len());
}
#[test]
fn knot_remove_roundtrip() {
let c = multi_span_curve();
let u_ins = 0.25;
let inserted = curve_knot_insert(&c, u_ins, 1).expect("insert valid");
let removed = curve_knot_remove(&inserted, u_ins, 1e-6).expect("remove valid");
assert_eq!(
removed.control_points().len(),
c.control_points().len(),
"control point count should match original"
);
assert_eq!(
removed.knots().len(),
c.knots().len(),
"knot count should match original"
);
for i in 0..=20 {
let u = i as f64 / 20.0;
let p1 = c.evaluate(u);
let p2 = removed.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-6
&& (p1.y() - p2.y()).abs() < 1e-6
&& (p1.z() - p2.z()).abs() < 1e-6,
"shape mismatch at u={u}: original=({},{},{}), removed=({},{},{})",
p1.x(),
p1.y(),
p1.z(),
p2.x(),
p2.y(),
p2.z()
);
}
}
#[test]
fn knot_remove_nonexistent_is_noop() {
let c = multi_span_curve();
let result = curve_knot_remove(&c, 0.123_456, 1e-6).expect("should succeed");
assert_eq!(result.knots().len(), c.knots().len());
assert_eq!(result.control_points().len(), c.control_points().len());
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = result.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-14
&& (p1.y() - p2.y()).abs() < 1e-14
&& (p1.z() - p2.z()).abs() < 1e-14,
"noop should preserve shape exactly at u={u}"
);
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_knot_insert_preserves_shape(u_insert in 0.01f64..0.99) {
let c = multi_span_curve();
let inserted = curve_knot_insert(&c, u_insert, 1).expect("valid");
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = inserted.evaluate(u);
prop_assert!(
(p1.x() - p2.x()).abs() < 1e-10
&& (p1.y() - p2.y()).abs() < 1e-10,
"mismatch at u={}: ({},{}) vs ({},{})",
u, p1.x(), p1.y(), p2.x(), p2.y()
);
}
}
#[test]
fn prop_knot_insert_remove_roundtrip(u_insert in 0.01f64..0.99) {
let c = multi_span_curve();
let inserted = curve_knot_insert(&c, u_insert, 1).expect("insert valid");
let removed = curve_knot_remove(&inserted, u_insert, 1e-4).expect("remove valid");
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = removed.evaluate(u);
prop_assert!(
(p1.x() - p2.x()).abs() < 1e-4
&& (p1.y() - p2.y()).abs() < 1e-4
&& (p1.z() - p2.z()).abs() < 1e-4,
"roundtrip mismatch at u={}: ({},{},{}) vs ({},{},{})",
u, p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z()
);
}
}
}
}