use crate::MathError;
use crate::aabb::Aabb3;
use crate::nurbs::curve::NurbsCurve;
use crate::nurbs::knot_ops::{surface_knot_insert_u, surface_knot_insert_v};
use crate::nurbs::surface::NurbsSurface;
use crate::vec::Point3;
pub fn curve_to_bezier_segments(curve: &NurbsCurve) -> Result<Vec<NurbsCurve>, MathError> {
let p = curve.degree();
let (d0, d1) = curve.domain();
let mut cuts: Vec<f64> = std::iter::once(d0)
.chain(curve.knots().iter().copied().filter(|&k| k > d0 && k < d1))
.chain(std::iter::once(d1))
.collect();
cuts.dedup_by(|a, b| (*a - *b).abs() <= 0.0);
if cuts.len() < 2 {
return Ok(vec![curve.clone()]);
}
let mut segments = Vec::with_capacity(cuts.len() - 1);
for w in cuts.windows(2) {
let homogeneous = curve_bezier_between(curve, w[0], w[1])?;
let mut points = Vec::with_capacity(p + 1);
let mut weights = Vec::with_capacity(p + 1);
for q in homogeneous {
points.push(cartesian(q).ok_or(MathError::PoleAtInfinity {
start: w[0],
end: w[1],
})?);
weights.push(q[3]);
}
let mut knots = vec![w[0]; p + 1];
knots.extend(std::iter::repeat_n(w[1], p + 1));
segments.push(NurbsCurve::new(p, knots, points, weights)?);
}
Ok(segments)
}
fn cartesian(q: [f64; 4]) -> Option<Point3> {
let point = if q[3] == 0.0 {
if q[..3].iter().any(|&c| c != 0.0) {
return None;
}
Point3::new(0.0, 0.0, 0.0)
} else {
Point3::new(q[0] / q[3], q[1] / q[3], q[2] / q[3])
};
[point.x(), point.y(), point.z()]
.iter()
.all(|c| c.is_finite())
.then_some(point)
}
pub fn curve_bezier_between(
curve: &NurbsCurve,
a: f64,
b: f64,
) -> Result<Vec<[f64; 4]>, MathError> {
let p = curve.degree();
let knots = curve.knots();
let n = curve.control_points().len();
let (lo, hi) = (a.min(b), a.max(b));
let (d0, d1) = curve.domain();
let out_of_range = MathError::ParameterOutOfRange {
value: if lo < d0 { lo } else { hi },
min: d0,
max: d1,
};
if n <= p {
return Err(out_of_range);
}
let i = p + knots[p..n].partition_point(|&k| k <= lo).saturating_sub(1);
if !(knots[i] <= lo && hi <= knots[i + 1] && knots[i] < knots[i + 1]) {
return Err(out_of_range);
}
let homogeneous: Vec<[f64; 4]> = curve.control_points()[i - p..=i]
.iter()
.zip(&curve.weights()[i - p..=i])
.map(|(pt, &w)| [pt.x() * w, pt.y() * w, pt.z() * w, w])
.collect();
let blossom = |at_b: usize| {
let mut d = homogeneous.clone();
for level in 1..=p {
let u = if level <= at_b { b } else { a };
for j in (level..=p).rev() {
let (left, right) = (knots[i - p + j], knots[i + 1 + j - level]);
let alpha = (u - left) / (right - left);
let prev = d[j - 1];
for (c, q) in d[j].iter_mut().zip(prev) {
*c = (1.0 - alpha).mul_add(q, alpha * *c);
}
}
}
d[p]
};
Ok((0..=p).map(blossom).collect())
}
#[derive(Debug, Clone)]
pub struct BezierPatch {
pub surface: NurbsSurface,
pub u_range: (f64, f64),
pub v_range: (f64, f64),
}
impl BezierPatch {
#[must_use]
pub fn aabb(&self) -> Aabb3 {
self.surface.aabb()
}
#[must_use]
pub fn diagonal(&self) -> f64 {
let aabb = self.aabb();
let dx = aabb.max.x() - aabb.min.x();
let dy = aabb.max.y() - aabb.min.y();
let dz = aabb.max.z() - aabb.min.z();
dx.mul_add(dx, dy.mul_add(dy, dz * dz)).sqrt()
}
#[must_use]
pub fn u_mid(&self) -> f64 {
(self.u_range.0 + self.u_range.1) * 0.5
}
#[must_use]
pub fn v_mid(&self) -> f64 {
(self.v_range.0 + self.v_range.1) * 0.5
}
}
#[allow(clippy::too_many_lines)]
pub fn surface_to_bezier_patches(surface: &NurbsSurface) -> Result<Vec<BezierPatch>, MathError> {
let pu = surface.degree_u();
let pv = surface.degree_v();
let mut refined = surface.clone();
{
let knots_u = refined.knots_u().to_vec();
let mut i = pu + 1;
while i < knots_u.len() - pu - 1 {
let u = knots_u[i];
let mut mult = 1;
while i + mult < knots_u.len() - pu - 1 && (knots_u[i + mult] - u).abs() < 1e-15 {
mult += 1;
}
let deficit = pu.saturating_sub(mult);
if deficit > 0 {
refined = surface_knot_insert_u(&refined, u, deficit)?;
}
i += mult;
}
}
{
let knots_v = refined.knots_v().to_vec();
let mut i = pv + 1;
while i < knots_v.len() - pv - 1 {
let v = knots_v[i];
let mut mult = 1;
while i + mult < knots_v.len() - pv - 1 && (knots_v[i + mult] - v).abs() < 1e-15 {
mult += 1;
}
let deficit = pv.saturating_sub(mult);
if deficit > 0 {
refined = surface_knot_insert_v(&refined, v, deficit)?;
}
i += mult;
}
}
let ref_knots_u = refined.knots_u();
let ref_knots_v = refined.knots_v();
let ref_cps = refined.control_points();
let ref_ws = refined.weights();
let n_rows = ref_cps.len();
let n_cols = if n_rows > 0 { ref_cps[0].len() } else { 0 };
let n_patches_u = if n_rows > pu { (n_rows - 1) / pu } else { 0 };
let n_patches_v = if n_cols > pv { (n_cols - 1) / pv } else { 0 };
if n_patches_u == 0 || n_patches_v == 0 {
let (u_start, u_end) = refined.domain_u();
let (v_start, v_end) = refined.domain_v();
return Ok(vec![BezierPatch {
surface: refined,
u_range: (u_start, u_end),
v_range: (v_start, v_end),
}]);
}
let mut patches = Vec::with_capacity(n_patches_u * n_patches_v);
for iu in 0..n_patches_u {
let row_start = iu * pu;
let row_end = row_start + pu + 1;
if row_end > n_rows {
break;
}
let u_start = ref_knots_u[row_start + pu];
let u_end = ref_knots_u[row_end];
for iv in 0..n_patches_v {
let col_start = iv * pv;
let col_end = col_start + pv + 1;
if col_end > n_cols {
break;
}
let v_start = ref_knots_v[col_start + pv];
let v_end = ref_knots_v[col_end];
let mut patch_cps = Vec::with_capacity(pu + 1);
let mut patch_ws = Vec::with_capacity(pu + 1);
for row in row_start..row_end {
patch_cps.push(ref_cps[row][col_start..col_end].to_vec());
patch_ws.push(ref_ws[row][col_start..col_end].to_vec());
}
let mut knots_u = vec![u_start; pu + 1];
knots_u.extend(std::iter::repeat_n(u_end, pu + 1));
let mut knots_v = vec![v_start; pv + 1];
knots_v.extend(std::iter::repeat_n(v_end, pv + 1));
let patch_surface = NurbsSurface::new(pu, pv, knots_u, knots_v, patch_cps, patch_ws)?;
patches.push(BezierPatch {
surface: patch_surface,
u_range: (u_start, u_end),
v_range: (v_start, v_end),
});
}
}
Ok(patches)
}
pub fn curve_degree_elevate(curve: &NurbsCurve, t: usize) -> Result<NurbsCurve, MathError> {
if t == 0 {
return Ok(curve.clone());
}
let p = curve.degree();
let new_p = p + t;
let segments = curve_to_bezier_segments(curve)?;
let n_segs = segments.len();
let bezalfs = compute_bezalfs(p, t);
let mut all_cps: Vec<[f64; 4]> = Vec::new();
let mut all_knots: Vec<f64> = Vec::new();
let knots = curve.knots();
let mut at = 0;
let joined: Vec<bool> = segments
.iter()
.take(n_segs.saturating_sub(1))
.map(|seg| {
let u = seg.knots()[p + 1];
while at < knots.len() && knots[at] < u {
at += 1;
}
let repeats = knots[at..]
.iter()
.take_while(|&&k| (k - u).abs() <= 0.0)
.count();
repeats <= p
})
.collect();
for (seg_idx, seg) in segments.iter().enumerate() {
let seg_cps = seg.control_points();
let seg_ws = seg.weights();
let seg_pw: Vec<[f64; 4]> = seg_cps
.iter()
.zip(seg_ws.iter())
.map(|(pt, &w)| [pt.x() * w, pt.y() * w, pt.z() * w, w])
.collect();
let mut elevated = vec![[0.0; 4]; new_p + 1];
for i in 0..=new_p {
for j in i.saturating_sub(t)..=(i.min(p)) {
if j <= p {
let coeff = bezalfs[i][j];
elevated[i][0] += coeff * seg_pw[j][0];
elevated[i][1] += coeff * seg_pw[j][1];
elevated[i][2] += coeff * seg_pw[j][2];
elevated[i][3] += coeff * seg_pw[j][3];
}
}
}
if seg_idx == 0 {
let u_start = seg.knots()[0];
for _ in 0..=new_p {
all_knots.push(u_start);
}
all_cps.extend_from_slice(&elevated);
} else if joined[seg_idx - 1] {
all_cps.extend_from_slice(&elevated[1..]);
} else {
all_cps.extend_from_slice(&elevated);
}
if seg_idx == n_segs - 1 {
let u_end = seg.knots()[p + 1];
for _ in 0..=new_p {
all_knots.push(u_end);
}
} else {
let u_break = seg.knots()[p + 1];
let copies = if joined[seg_idx] { new_p } else { new_p + 1 };
for _ in 0..copies {
all_knots.push(u_break);
}
}
}
if segments.is_empty() {
return Ok(curve.clone());
}
let new_cps: Vec<Point3> = all_cps
.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> = all_cps.iter().map(|h| h[3]).collect();
NurbsCurve::new(new_p, all_knots, new_cps, new_ws)
}
#[allow(clippy::cast_precision_loss)]
fn compute_bezalfs(p: usize, t: usize) -> Vec<Vec<f64>> {
let new_p = p + t;
let mut bezalfs = vec![vec![0.0; p + 1]; new_p + 1];
for (i, row) in bezalfs.iter_mut().enumerate().take(new_p + 1) {
let j_min = i.saturating_sub(t);
let j_max = i.min(p);
for (j, val) in row.iter_mut().enumerate().take(j_max + 1).skip(j_min) {
*val = binomial(p, j) as f64 * binomial(t, i - j) as f64 / binomial(new_p, i) as f64;
}
}
bezalfs
}
use super::basis::binomial;
#[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 bezier_decompose_single_span() {
let c = cubic_bezier();
let segs = curve_to_bezier_segments(&c).expect("valid");
assert_eq!(segs.len(), 1);
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = segs[0].evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-12 && (p1.y() - p2.y()).abs() < 1e-12,
"mismatch at u={u}"
);
}
}
#[test]
fn bezier_decompose_multi_span() {
let c = multi_span_curve();
let segs = curve_to_bezier_segments(&c).expect("valid");
assert!(segs.len() >= 2, "expected at least 2 segments");
for seg in &segs {
assert_eq!(seg.degree(), 3);
assert_eq!(seg.control_points().len(), 4);
}
}
#[test]
fn degree_elevate_preserves_shape() {
let c = cubic_bezier();
let elevated = curve_degree_elevate(&c, 1).expect("valid");
assert_eq!(elevated.degree(), 4);
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = elevated.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-10 && (p1.y() - p2.y()).abs() < 1e-10,
"mismatch at u={u}: ({},{}) vs ({},{})",
p1.x(),
p1.y(),
p2.x(),
p2.y()
);
}
}
#[test]
fn degree_elevate_by_two() {
let c = cubic_bezier();
let elevated = curve_degree_elevate(&c, 2).expect("valid");
assert_eq!(elevated.degree(), 5);
for i in 0..=10 {
let u = i as f64 / 10.0;
let p1 = c.evaluate(u);
let p2 = elevated.evaluate(u);
assert!(
(p1.x() - p2.x()).abs() < 1e-9 && (p1.y() - p2.y()).abs() < 1e-9,
"mismatch at u={u}"
);
}
}
fn bilinear_surface() -> NurbsSurface {
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")
}
fn quadratic_surface_multi_span() -> NurbsSurface {
NurbsSurface::new(
2,
2,
vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0],
vec![
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.25, 0.0, 0.1),
Point3::new(0.5, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
],
vec![
Point3::new(0.0, 0.25, 0.1),
Point3::new(0.25, 0.25, 0.5),
Point3::new(0.5, 0.25, 0.1),
Point3::new(1.0, 0.25, 0.1),
],
vec![
Point3::new(0.0, 0.5, 0.0),
Point3::new(0.25, 0.5, 0.1),
Point3::new(0.5, 0.5, 0.0),
Point3::new(1.0, 0.5, 0.0),
],
vec![
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.25, 1.0, 0.0),
Point3::new(0.5, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
],
],
vec![vec![1.0; 4]; 4],
)
.expect("valid")
}
#[test]
fn bezier_patches_single_span() {
let surf = bilinear_surface();
let patches = surface_to_bezier_patches(&surf).expect("valid");
assert_eq!(patches.len(), 1, "single-span surface = 1 patch");
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 = patches[0].surface.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,
"mismatch at ({u}, {v})"
);
}
}
}
#[test]
fn bezier_patches_cover_surface() {
let surf = quadratic_surface_multi_span();
let patches = surface_to_bezier_patches(&surf).expect("valid");
assert!(
patches.len() >= 4,
"expected at least 4 patches, got {}",
patches.len()
);
for patch in &patches {
assert_eq!(patch.surface.degree_u(), 2);
assert_eq!(patch.surface.degree_v(), 2);
assert_eq!(patch.surface.control_points().len(), 3);
assert_eq!(patch.surface.control_points()[0].len(), 3);
}
for patch in &patches {
let u_mid = (patch.u_range.0 + patch.u_range.1) * 0.5;
let v_mid = (patch.v_range.0 + patch.v_range.1) * 0.5;
let p_orig = surf.evaluate(u_mid, v_mid);
let patch_u = (u_mid - patch.u_range.0) / (patch.u_range.1 - patch.u_range.0);
let patch_v = (v_mid - patch.v_range.0) / (patch.v_range.1 - patch.v_range.0);
let pu = patch.surface.domain_u();
let pv = patch.surface.domain_v();
let local_u = pu.0 + patch_u * (pu.1 - pu.0);
let local_v = pv.0 + patch_v * (pv.1 - pv.0);
let p_patch = patch.surface.evaluate(local_u, local_v);
assert!(
(p_orig.x() - p_patch.x()).abs() < 1e-8
&& (p_orig.y() - p_patch.y()).abs() < 1e-8
&& (p_orig.z() - p_patch.z()).abs() < 1e-8,
"patch mismatch at ({u_mid}, {v_mid}): orig=({},{},{}), patch=({},{},{})",
p_orig.x(),
p_orig.y(),
p_orig.z(),
p_patch.x(),
p_patch.y(),
p_patch.z()
);
}
}
#[test]
fn bezier_patch_aabb_and_diagonal() {
let surf = bilinear_surface();
let patches = surface_to_bezier_patches(&surf).expect("valid");
let patch = &patches[0];
let diag = patch.diagonal();
assert!(
(diag - std::f64::consts::SQRT_2).abs() < 0.1,
"expected diagonal ≈ √2, got {diag}"
);
}
fn bezier_at(q: &[[f64; 4]], s: f64) -> Point3 {
let mut d = q.to_vec();
for r in 1..d.len() {
for j in 0..d.len() - r {
for c in 0..4 {
d[j][c] = (1.0 - s) * d[j][c] + s * d[j + 1][c];
}
}
}
let [x, y, z, w] = d[0];
Point3::new(x / w, y / w, z / w)
}
fn unclamped_rational_cubic() -> NurbsCurve {
NurbsCurve::new(
3,
vec![-0.3, -0.1, 0.0, 0.2, 0.5, 0.7, 1.0, 1.2, 1.4, 1.5],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.5),
Point3::new(2.0, -1.0, 1.0),
Point3::new(3.0, 2.0, 0.0),
Point3::new(4.0, 0.5, -1.0),
Point3::new(5.0, 1.0, 0.0),
],
vec![1.0, 0.7, 1.3, 0.9, 1.1, 0.8],
)
.expect("valid")
}
#[test]
fn bezier_between_traces_the_curve_between_its_parameters() {
let curve = unclamped_rational_cubic();
for (a, b) in [
(0.25, 0.45),
(0.45, 0.25),
(0.5, 0.7),
(0.7, 1.0),
(1.0, 0.7),
] {
let q = curve_bezier_between(&curve, a, b).expect("one span");
assert_eq!(q.len(), 4);
for k in 0..=8 {
let s = f64::from(k) / 8.0;
let on_curve = curve.evaluate(a + s * (b - a));
let gap = (bezier_at(&q, s) - on_curve).length();
assert!(gap < 1e-12, "({a}, {b}) at {s}: {gap}");
}
}
}
#[test]
fn bezier_between_refuses_more_than_one_span() {
let curve = unclamped_rational_cubic();
assert!(curve_bezier_between(&curve, 0.4, 0.6).is_err());
assert!(curve_bezier_between(&curve, 0.1, 0.3).is_err());
assert!(curve_bezier_between(&curve, 0.9, 1.1).is_err());
}
fn pieces_trace(c: &NurbsCurve, spans: usize) {
let segs = curve_to_bezier_segments(c).expect("valid");
assert_eq!(segs.len(), spans);
for seg in &segs {
let (a, b) = seg.domain();
for k in 1..8 {
let t = (b - a).mul_add(f64::from(k) / 8.0, a);
let off = (seg.evaluate(t) - c.evaluate(t)).length();
assert!(off < 1e-12, "at {t}: {off}");
}
}
}
#[test]
fn a_knot_repeated_past_the_degree_cuts_two_pieces() {
let p = Point3::new;
let c = NurbsCurve::new(
1,
vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0],
vec![
p(1.0, 1.0, 0.0),
p(0.0, 1.0, 0.0),
p(0.0, 1.0, 0.0),
p(-1.0, 1.0, 0.0),
],
vec![1.0; 4],
)
.expect("valid");
pieces_trace(&c, 2);
}
#[test]
fn a_degree_zero_curve_cuts_into_its_spans() {
let p = Point3::new;
let c = NurbsCurve::new(
0,
vec![0.0, 0.5, 1.0],
vec![p(0.0, 0.0, 0.0), p(1.0, 0.0, 0.0)],
vec![1.0; 2],
)
.expect("valid");
pieces_trace(&c, 2);
}
#[test]
fn a_curve_not_clamped_cuts_into_its_domains_spans() {
let c = NurbsCurve::new(
3,
(0..8).map(f64::from).collect(),
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
Point3::new(3.0, -1.0, 1.0),
Point3::new(4.0, 1.0, 0.0),
],
vec![1.0, 2.0, 0.5, 1.0],
)
.expect("valid");
pieces_trace(&c, 1);
}
#[test]
fn a_zero_weight_pole_cuts_a_piece_that_traces_the_curve() {
let p = Point3::new;
let c = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![p(0.0, 0.0, 0.0), p(1.0, 1.0, 0.0), p(2.0, 0.0, 0.0)],
vec![1.0, 0.0, 1.0],
)
.expect("valid");
pieces_trace(&c, 1);
}
#[test]
fn a_signed_zero_knot_cuts_no_piece_of_no_width() {
let p = Point3::new;
let c = NurbsCurve::new(
2,
vec![-1.0, -1.0, -1.0, -0.0, 0.0, 1.0, 1.0, 1.0],
vec![
p(0.0, 0.0, 0.0),
p(1.0, 1.0, 0.0),
p(2.0, 0.0, 0.0),
p(3.0, 1.0, 0.0),
p(4.0, 0.0, 0.0),
],
vec![1.0; 5],
)
.expect("valid");
pieces_trace(&c, 2);
}
#[test]
fn a_curve_raised_in_degree_keeps_its_jump() {
let p = Point3::new;
let c = NurbsCurve::new(
1,
vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0],
vec![
p(0.0, 0.0, 0.0),
p(1.0, 0.0, 0.0),
p(1.0, 2.0, 0.0),
p(2.0, 2.0, 0.0),
],
vec![1.0; 4],
)
.expect("valid");
let raised = curve_degree_elevate(&c, 2).expect("valid");
assert_eq!(raised.degree(), 3);
for t in [0.1, 0.25, 0.4, 0.6, 0.75, 0.9] {
let off = (raised.evaluate(t) - c.evaluate(t)).length();
assert!(off < 1e-12, "at {t}: {off}");
}
}
#[test]
fn a_jump_raised_in_degree_does_not_hang_on_the_weights_scale() {
let p = Point3::new;
for w in [1.0, 1e-14, 1e14] {
let c = NurbsCurve::new(
1,
vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0],
vec![
p(0.0, 0.0, 0.0),
p(1.0, 0.0, 0.0),
p(10.0, 0.0, 0.0),
p(11.0, 0.0, 0.0),
],
vec![w; 4],
)
.expect("valid");
let raised = curve_degree_elevate(&c, 2).expect("valid");
for t in [0.1, 0.25, 0.4, 0.6, 0.75, 0.9] {
let off = (raised.evaluate(t) - c.evaluate(t)).length();
assert!(off < 1e-9, "weights {w}, at {t}: {off}");
}
}
}
#[test]
fn a_span_whose_weights_cancel_to_a_pole_at_infinity_cuts_no_piece() {
let p = Point3::new;
let c = NurbsCurve::new(
3,
vec![0.0, 0.0, 1.0, 2.0, 3.0, 5.0, 5.0, 5.0],
vec![
p(0.0, 0.0, 0.0),
p(1.0, 0.0, 0.0),
p(2.0, 0.0, 0.0),
p(3.0, 0.0, 0.0),
],
vec![10.0, -1.0, 3.0, 1.0],
)
.expect("valid");
let q = curve_bezier_between(&c, 2.0, 3.0).expect("in one span");
assert!(
q.iter().any(|h| h[3] == 0.0 && h[0] != 0.0),
"no pole at infinity: {q:?}"
);
assert!(matches!(
curve_to_bezier_segments(&c),
Err(MathError::PoleAtInfinity { .. })
));
}
#[test]
fn a_curve_continuous_at_its_knots_raised_in_degree_stays_joined() {
let p = Point3::new;
let c = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0],
vec![
p(0.0, 0.0, 0.0),
p(1.0, 1.0, 0.0),
p(2.0, 0.0, 0.0),
p(3.0, 1.0, 0.0),
p(4.0, 0.0, 0.0),
],
vec![1.0, 2.0, 1.0, 0.5, 1.0],
)
.expect("valid");
let raised = curve_degree_elevate(&c, 1).expect("valid");
assert_eq!(raised.control_points().len(), 7);
for t in [0.1, 0.25, 0.4, 0.5, 0.6, 0.75, 0.9] {
let off = (raised.evaluate(t) - c.evaluate(t)).length();
assert!(off < 1e-12, "at {t}: {off}");
}
}
}