1use std::f64::consts::PI;
6
7use super::types::{BSplineCurve, BezierCurve};
8
9#[inline]
11pub fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
12 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
13}
14#[inline]
16pub fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19#[inline]
21pub fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
22 [a[0] * s, a[1] * s, a[2] * s]
23}
24#[inline]
26pub fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
27 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
28}
29#[inline]
31pub fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
32 [
33 a[1] * b[2] - a[2] * b[1],
34 a[2] * b[0] - a[0] * b[2],
35 a[0] * b[1] - a[1] * b[0],
36 ]
37}
38#[inline]
40pub fn vec3_norm(a: [f64; 3]) -> f64 {
41 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
42}
43#[inline]
45pub fn vec3_normalize(a: [f64; 3]) -> [f64; 3] {
46 let n = vec3_norm(a);
47 if n < 1e-300 {
48 [0.0, 0.0, 0.0]
49 } else {
50 vec3_scale(a, 1.0 / n)
51 }
52}
53#[inline]
55pub fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
56 vec3_add(vec3_scale(a, 1.0 - t), vec3_scale(b, t))
57}
58pub fn bspline_basis(span: usize, p: usize, t: f64, knots: &[f64]) -> Vec<f64> {
67 let mut n = vec![0.0f64; p + 1];
68 let mut left = vec![0.0f64; p + 1];
69 let mut right = vec![0.0f64; p + 1];
70 n[0] = 1.0;
71 for j in 1..=p {
72 left[j] = t - knots[span + 1 - j];
73 right[j] = knots[span + j] - t;
74 let mut saved = 0.0f64;
75 for r in 0..j {
76 let temp = n[r] / (right[r + 1] + left[j - r]);
77 n[r] = saved + right[r + 1] * temp;
78 saved = left[j - r] * temp;
79 }
80 n[j] = saved;
81 }
82 n
83}
84pub fn find_knot_span(num_ctrl: usize, p: usize, t: f64, knots: &[f64]) -> usize {
87 let n = num_ctrl - 1;
88 if t >= knots[n + 1] {
89 return n;
90 }
91 if t <= knots[p] {
92 return p;
93 }
94 let mut low = p;
95 let mut high = n + 1;
96 let mut mid = (low + high) / 2;
97 while t < knots[mid] || t >= knots[mid + 1] {
98 if t < knots[mid] {
99 high = mid;
100 } else {
101 low = mid;
102 }
103 mid = (low + high) / 2;
104 }
105 mid
106}
107pub fn uniform_clamped_knots(num_ctrl: usize, p: usize) -> Vec<f64> {
111 let n = num_ctrl - 1;
112 let m = n + p + 1;
113 let mut knots = vec![0.0f64; m + 1];
114 for (i, k) in knots.iter_mut().enumerate().take(m + 1) {
115 if i <= p {
116 *k = 0.0;
117 } else if i > n {
118 *k = 1.0;
119 } else {
120 *k = (i - p) as f64 / (n - p + 1) as f64;
121 }
122 }
123 knots
124}
125pub fn fit_bspline_least_squares(
131 data: &[[f64; 3]],
132 num_ctrl: usize,
133 degree: usize,
134) -> BSplineCurve {
135 let m = data.len();
136 assert!(m >= num_ctrl, "need at least num_ctrl data points");
137 let knots = uniform_clamped_knots(num_ctrl, degree);
138 let mut params = vec![0.0f64; m];
139 for i in 1..m {
140 params[i] = params[i - 1] + vec3_norm(vec3_sub(data[i], data[i - 1]));
141 }
142 let total = params[m - 1];
143 if total > 0.0 {
144 for p in params.iter_mut() {
145 *p /= total;
146 }
147 }
148 let mut a_mat = vec![vec![0.0f64; num_ctrl]; m];
149 for (row, &t) in params.iter().enumerate() {
150 let span = find_knot_span(num_ctrl, degree, t, &knots);
151 let basis = bspline_basis(span, degree, t, &knots);
152 for j in 0..=degree {
153 a_mat[row][span - degree + j] = basis[j];
154 }
155 }
156 let mut ata = vec![vec![0.0f64; num_ctrl]; num_ctrl];
157 let mut atb = vec![[0.0f64; 3]; num_ctrl];
158 for i in 0..m {
159 for j in 0..num_ctrl {
160 for k in 0..num_ctrl {
161 ata[j][k] += a_mat[i][j] * a_mat[i][k];
162 }
163 for d in 0..3 {
164 atb[j][d] += a_mat[i][j] * data[i][d];
165 }
166 }
167 }
168 let n = num_ctrl;
169 let mut aug = vec![vec![0.0f64; n + 3]; n];
170 for i in 0..n {
171 for j in 0..n {
172 aug[i][j] = ata[i][j];
173 }
174 for d in 0..3 {
175 aug[i][n + d] = atb[i][d];
176 }
177 }
178 for col in 0..n {
179 let mut max_row = col;
180 let mut max_val = aug[col][col].abs();
181 for (row, _) in aug.iter().enumerate().take(n).skip(col + 1) {
182 if aug[row][col].abs() > max_val {
183 max_val = aug[row][col].abs();
184 max_row = row;
185 }
186 }
187 aug.swap(col, max_row);
188 let pivot = aug[col][col];
189 if pivot.abs() < 1e-12 {
190 continue;
191 }
192 for elem in aug[col].iter_mut().skip(col) {
193 *elem /= pivot;
194 }
195 for row in 0..n {
196 if row != col {
197 let factor = aug[row][col];
198 let col_row: Vec<f64> = aug[col][col..].to_vec();
199 for (j_off, elem) in aug[row][col..].iter_mut().enumerate() {
200 *elem -= factor * col_row[j_off];
201 }
202 }
203 }
204 }
205 let ctrl: Vec<[f64; 3]> = (0..n)
206 .map(|i| [aug[i][n], aug[i][n + 1], aug[i][n + 2]])
207 .collect();
208 BSplineCurve::new(ctrl, degree, knots)
209}
210pub fn surface_of_revolution(
215 profile: &[[f64; 2]],
216 num_angular_samples: usize,
217) -> Vec<Vec<[f64; 3]>> {
218 let m = profile.len();
219 let n = num_angular_samples;
220 let mut grid = vec![vec![[0.0f64; 3]; n]; m];
221 for (i, &[r, z]) in profile.iter().enumerate() {
222 for (j, cell) in grid[i].iter_mut().enumerate() {
223 let theta = 2.0 * PI * j as f64 / n as f64;
224 *cell = [r * theta.cos(), r * theta.sin(), z];
225 }
226 }
227 grid
228}
229pub fn lofted_surface(sections: &[Vec<[f64; 3]>], num_v_samples: usize) -> Vec<Vec<[f64; 3]>> {
233 let ns = sections.len();
234 assert!(ns >= 2, "need at least 2 sections");
235 let np = sections[0].len();
236 for s in sections.iter() {
237 assert_eq!(s.len(), np, "all sections must have equal point count");
238 }
239 let mut grid = vec![vec![[0.0f64; 3]; np]; num_v_samples];
240 for (vi, row) in grid.iter_mut().enumerate() {
241 let t = vi as f64 / (num_v_samples - 1).max(1) as f64;
242 let seg = ((t * (ns - 1) as f64).min((ns - 2) as f64)) as usize;
243 let local_t = t * (ns - 1) as f64 - seg as f64;
244 for (pi, cell) in row.iter_mut().enumerate() {
245 *cell = vec3_lerp(sections[seg][pi], sections[seg + 1][pi], local_t);
246 }
247 }
248 grid
249}
250pub fn curve_curvature(curve: &BezierCurve, t: f64) -> f64 {
254 let r_prime = curve.tangent(t);
255 let r_double = curve.second_derivative(t);
256 let cross = vec3_cross(r_prime, r_double);
257 let cross_norm = vec3_norm(cross);
258 let prime_norm = vec3_norm(r_prime);
259 if prime_norm < 1e-12 {
260 0.0
261 } else {
262 cross_norm / (prime_norm * prime_norm * prime_norm)
263 }
264}
265pub fn curve_torsion(curve: &BezierCurve, t: f64, h: f64) -> f64 {
270 let r_prime = curve.tangent(t);
271 let r_double = curve.second_derivative(t);
272 let r_double_plus = {
273 let tp = (t + h).min(1.0);
274 curve.second_derivative(tp)
275 };
276 let r_double_minus = {
277 let tm = (t - h).max(0.0);
278 curve.second_derivative(tm)
279 };
280 let r_triple = vec3_scale(vec3_sub(r_double_plus, r_double_minus), 1.0 / (2.0 * h));
281 let cross = vec3_cross(r_prime, r_double);
282 let denom = vec3_dot(cross, cross);
283 if denom < 1e-20 {
284 0.0
285 } else {
286 vec3_dot(cross, r_triple) / denom
287 }
288}
289pub fn frenet_serret_frame(curve: &BezierCurve, t: f64) -> ([f64; 3], [f64; 3], [f64; 3]) {
293 let t_vec = vec3_normalize(curve.tangent(t));
294 let r_double = curve.second_derivative(t);
295 let n_raw = vec3_sub(r_double, vec3_scale(t_vec, vec3_dot(r_double, t_vec)));
296 let n_vec = vec3_normalize(n_raw);
297 let b_vec = vec3_cross(t_vec, n_vec);
298 (t_vec, n_vec, b_vec)
299}
300pub fn osculating_radius(curve: &BezierCurve, t: f64) -> f64 {
302 let kappa = curve_curvature(curve, t);
303 if kappa < 1e-12 {
304 f64::INFINITY
305 } else {
306 1.0 / kappa
307 }
308}
309pub fn chord_length_params(points: &[[f64; 3]]) -> Vec<f64> {
313 let n = points.len();
314 if n == 0 {
315 return vec![];
316 }
317 let mut params = vec![0.0f64; n];
318 for i in 1..n {
319 params[i] = params[i - 1] + vec3_norm(vec3_sub(points[i], points[i - 1]));
320 }
321 let total = params[n - 1];
322 if total > 0.0 {
323 for p in params.iter_mut() {
324 *p /= total;
325 }
326 }
327 params
328}
329pub fn knot_insert(curve: &BSplineCurve, t_new: f64) -> BSplineCurve {
331 let n = curve.control_points.len();
332 let p = curve.degree;
333 let knots = &curve.knots;
334 let span = find_knot_span(n, p, t_new, knots);
335 let mut new_knots = Vec::with_capacity(knots.len() + 1);
336 new_knots.extend_from_slice(&knots[..span + 1]);
337 new_knots.push(t_new);
338 new_knots.extend_from_slice(&knots[span + 1..]);
339 let mut new_cp = Vec::with_capacity(n + 1);
340 for i in 0..=span - p {
341 new_cp.push(curve.control_points[i]);
342 }
343 for i in span - p + 1..=span {
344 let alpha = (t_new - knots[i]) / (knots[i + p] - knots[i]);
345 let prev = curve.control_points[i - 1];
346 let curr = curve.control_points[i];
347 new_cp.push(vec3_add(
348 vec3_scale(prev, 1.0 - alpha),
349 vec3_scale(curr, alpha),
350 ));
351 }
352 for i in span..n {
353 new_cp.push(curve.control_points[i]);
354 }
355 BSplineCurve::new(new_cp, p, new_knots)
356}
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::spline_geometry::BSplineSurface;
361 use crate::spline_geometry::BezierPatch;
362 use crate::spline_geometry::BlendingSurface;
363 use crate::spline_geometry::ContinuityOrder;
364 use crate::spline_geometry::CubicSpline;
365 use crate::spline_geometry::NurbsCurve;
366 use crate::spline_geometry::NurbsSurface;
367 use crate::spline_geometry::PeriodicBSpline;
368 use crate::spline_geometry::SweptSurface;
369 pub(super) const EPS: f64 = 1e-9;
370 #[test]
371 fn test_bspline_basis_partition_of_unity() {
372 let ctrl = [
373 [0.0, 0.0, 0.0],
374 [1.0, 0.0, 0.0],
375 [2.0, 1.0, 0.0],
376 [3.0, 0.0, 0.0],
377 [4.0, 0.0, 0.0],
378 ];
379 let degree = 3;
380 let knots = uniform_clamped_knots(ctrl.len(), degree);
381 for i in 1..10 {
382 let t = i as f64 / 10.0;
383 let span = find_knot_span(ctrl.len(), degree, t, &knots);
384 let basis = bspline_basis(span, degree, t, &knots);
385 let sum: f64 = basis.iter().sum();
386 assert!((sum - 1.0).abs() < 1e-10, "basis sum = {}", sum);
387 }
388 }
389 #[test]
390 fn test_bspline_basis_non_negative() {
391 let ctrl = [
392 [0.0, 0.0, 0.0],
393 [1.0, 0.0, 0.0],
394 [2.0, 1.0, 0.0],
395 [3.0, 0.0, 0.0],
396 ];
397 let degree = 2;
398 let knots = uniform_clamped_knots(ctrl.len(), degree);
399 for i in 0..=20 {
400 let t = i as f64 / 20.0;
401 let t_clamped = t.min(1.0 - 1e-12);
402 let span = find_knot_span(ctrl.len(), degree, t_clamped, &knots);
403 let basis = bspline_basis(span, degree, t_clamped, &knots);
404 for &b in &basis {
405 assert!(b >= -1e-12, "negative basis value: {}", b);
406 }
407 }
408 }
409 #[test]
410 fn test_uniform_clamped_knots_endpoints() {
411 let knots = uniform_clamped_knots(5, 3);
412 for &k in knots.iter().take(4) {
413 assert_eq!(k, 0.0);
414 }
415 let n = knots.len();
416 for &k in knots.iter().skip(n - 4) {
417 assert_eq!(k, 1.0);
418 }
419 }
420 #[test]
421 fn test_find_knot_span_boundary() {
422 let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
423 let span = find_knot_span(3, 2, 0.5, &knots);
424 assert_eq!(span, 2);
425 }
426 #[test]
427 fn test_bspline_curve_endpoints() {
428 let ctrl = vec![
429 [0.0, 0.0, 0.0],
430 [1.0, 2.0, 0.0],
431 [3.0, 2.0, 0.0],
432 [4.0, 0.0, 0.0],
433 ];
434 let curve = BSplineCurve::with_clamped_knots(ctrl.clone(), 3);
435 let p0 = curve.eval(0.0);
436 let p1 = curve.eval(1.0 - 1e-12);
437 assert!((p0[0] - ctrl[0][0]).abs() < 1e-6);
438 assert!((p1[0] - ctrl[3][0]).abs() < 1e-4, "p1.x = {}", p1[0]);
439 }
440 #[test]
441 fn test_bspline_curve_linear_reproduces_line() {
442 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
443 let curve = BSplineCurve::with_clamped_knots(ctrl, 1);
444 for i in 0..=10 {
445 let t = i as f64 / 10.0;
446 let p = curve.eval(t.min(1.0 - 1e-12));
447 assert!((p[0] - p[1]).abs() < 1e-10);
448 }
449 }
450 #[test]
451 fn test_bspline_curve_arc_length_positive() {
452 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
453 let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
454 let len = curve.arc_length(100);
455 assert!(len > 0.0);
456 }
457 #[test]
458 fn test_bspline_curve_derivative_direction() {
459 let ctrl = vec![
460 [0.0, 0.0, 0.0],
461 [1.0, 0.0, 0.0],
462 [2.0, 0.0, 0.0],
463 [3.0, 0.0, 0.0],
464 ];
465 let curve = BSplineCurve::with_clamped_knots(ctrl, 3);
466 let d = curve.derivative(0.5);
467 assert!(d[0] > 0.0, "tangent x = {}", d[0]);
468 assert!(d[1].abs() < 1e-6);
469 }
470 #[test]
471 fn test_bspline_curve_sample_count() {
472 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
473 let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
474 let pts = curve.sample(11);
475 assert_eq!(pts.len(), 11);
476 }
477 #[test]
478 fn test_nurbs_unit_weights_matches_bspline() {
479 let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
480 let weights = vec![1.0, 1.0, 1.0];
481 let nurbs = NurbsCurve::from_points_and_weights(pts.clone(), weights, 2);
482 let bsp = BSplineCurve::with_clamped_knots(pts, 2);
483 for i in 1..10 {
484 let t = i as f64 / 10.0;
485 let pn = nurbs.eval(t);
486 let pb = bsp.eval(t);
487 for k in 0..3 {
488 assert!(
489 (pn[k] - pb[k]).abs() < 1e-9,
490 "mismatch at dim {}: {} vs {}",
491 k,
492 pn[k],
493 pb[k]
494 );
495 }
496 }
497 }
498 #[test]
499 fn test_nurbs_circle_radius() {
500 let r = 3.0;
501 let circle = NurbsCurve::circle(r);
502 for i in 0..12 {
503 let t = i as f64 / 12.0;
504 let p = circle.eval(t);
505 let dist = (p[0] * p[0] + p[1] * p[1]).sqrt();
506 assert!(
507 (dist - r).abs() < 1e-6,
508 "circle radius error: {} vs {}",
509 dist,
510 r
511 );
512 }
513 }
514 #[test]
515 fn test_nurbs_sample_count() {
516 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
517 let nurbs = NurbsCurve::from_points_and_weights(pts, vec![1.0, 1.0, 1.0], 2);
518 assert_eq!(nurbs.sample(7).len(), 7);
519 }
520 #[test]
521 fn test_bezier_endpoints() {
522 let ctrl = vec![
523 [0.0, 0.0, 0.0],
524 [1.0, 2.0, 0.0],
525 [3.0, 2.0, 0.0],
526 [4.0, 0.0, 0.0],
527 ];
528 let curve = BezierCurve::new(ctrl.clone());
529 let p0 = curve.eval(0.0);
530 let p1 = curve.eval(1.0);
531 for k in 0..3 {
532 assert!((p0[k] - ctrl[0][k]).abs() < EPS);
533 assert!((p1[k] - ctrl[3][k]).abs() < EPS);
534 }
535 }
536 #[test]
537 fn test_bezier_convex_hull_property() {
538 let ctrl = vec![
539 [0.0, 0.0, 0.0],
540 [1.0, 2.0, 0.0],
541 [3.0, 2.0, 0.0],
542 [4.0, 0.0, 0.0],
543 ];
544 let curve = BezierCurve::new(ctrl.clone());
545 let x_min = ctrl.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
546 let x_max = ctrl.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max);
547 let y_min = ctrl.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min);
548 let y_max = ctrl.iter().map(|p| p[1]).fold(f64::NEG_INFINITY, f64::max);
549 for i in 0..=20 {
550 let t = i as f64 / 20.0;
551 let p = curve.eval(t);
552 assert!(p[0] >= x_min - 1e-9 && p[0] <= x_max + 1e-9);
553 assert!(p[1] >= y_min - 1e-9 && p[1] <= y_max + 1e-9);
554 }
555 }
556 #[test]
557 fn test_bezier_linear_is_interpolating() {
558 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
559 let curve = BezierCurve::new(ctrl);
560 let mid = curve.eval(0.5);
561 assert!((mid[0] - 0.5).abs() < EPS);
562 assert!((mid[1] - 0.5).abs() < EPS);
563 }
564 #[test]
565 fn test_bezier_split_reconstructs() {
566 let ctrl = vec![
567 [0.0, 0.0, 0.0],
568 [1.0, 2.0, 0.0],
569 [3.0, 2.0, 0.0],
570 [4.0, 0.0, 0.0],
571 ];
572 let curve = BezierCurve::new(ctrl);
573 let (left, right) = curve.split(0.5);
574 let orig = BezierCurve::new(curve.control_points.clone()).eval(0.5);
575 let lp = left.eval(1.0);
576 let rp = right.eval(0.0);
577 for k in 0..3 {
578 assert!((orig[k] - lp[k]).abs() < 1e-9);
579 assert!((orig[k] - rp[k]).abs() < 1e-9);
580 }
581 }
582 #[test]
583 fn test_bezier_degree_elevate_same_shape() {
584 let ctrl = vec![[0.0, 0.0, 0.0], [2.0, 4.0, 0.0], [4.0, 0.0, 0.0]];
585 let curve = BezierCurve::new(ctrl);
586 let elevated = curve.degree_elevate();
587 assert_eq!(elevated.control_points.len(), 4);
588 for i in 0..=10 {
589 let t = i as f64 / 10.0;
590 let p_orig = curve.eval(t);
591 let p_elev = elevated.eval(t);
592 for k in 0..3 {
593 assert!(
594 (p_orig[k] - p_elev[k]).abs() < 1e-9,
595 "degree elevation changed shape at t={}: {} vs {}",
596 t,
597 p_orig[k],
598 p_elev[k]
599 );
600 }
601 }
602 }
603 #[test]
604 fn test_bezier_arc_length_positive() {
605 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
606 let curve = BezierCurve::new(ctrl);
607 assert!(curve.arc_length(100) > 0.0);
608 }
609 #[test]
610 fn test_natural_spline_interpolates_data() {
611 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
612 let y = vec![0.0, 1.0, 0.0, -1.0, 0.0];
613 let spline = CubicSpline::natural(x.clone(), y.clone());
614 for i in 0..x.len() {
615 let val = spline.eval(x[i]);
616 assert!(
617 (val - y[i]).abs() < 1e-8,
618 "spline({}) = {} vs {}",
619 x[i],
620 val,
621 y[i]
622 );
623 }
624 }
625 #[test]
626 fn test_clamped_spline_interpolates_data() {
627 let x = vec![0.0, 1.0, 2.0, 3.0];
628 let y = vec![0.0, 1.0, 4.0, 9.0];
629 let spline = CubicSpline::clamped(x.clone(), y.clone(), 1.0, 6.0);
630 for i in 0..x.len() {
631 let val = spline.eval(x[i]);
632 assert!((val - y[i]).abs() < 1e-8);
633 }
634 }
635 #[test]
636 fn test_natural_spline_second_deriv_at_endpoints_zero() {
637 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
638 let y = vec![1.0, 2.0, 1.5, 3.0, 2.0];
639 let spline = CubicSpline::natural(x.clone(), y.clone());
640 let d2_left = spline.eval_second_derivative(x[0]);
641 let d2_right = spline.eval_second_derivative(*x.last().unwrap());
642 assert!(d2_left.abs() < 1e-8, "d2 at left = {}", d2_left);
643 assert!(d2_right.abs() < 1e-8, "d2 at right = {}", d2_right);
644 }
645 #[test]
646 fn test_spline_eval_derivative_finite_diff() {
647 let x = vec![0.0, 1.0, 2.0, 3.0];
648 let y = vec![0.0, 1.0, 0.0, 1.0];
649 let spline = CubicSpline::natural(x, y);
650 let t = 1.5;
651 let h = 1e-5;
652 let fd = (spline.eval(t + h) - spline.eval(t - h)) / (2.0 * h);
653 let exact = spline.eval_derivative(t);
654 assert!((fd - exact).abs() < 1e-4, "fd={} exact={}", fd, exact);
655 }
656 #[test]
657 fn test_cubic_spline_linear_data() {
658 let x = vec![0.0, 1.0, 2.0, 3.0];
659 let y = vec![0.0, 2.0, 4.0, 6.0];
660 let spline = CubicSpline::natural(x, y);
661 let val = spline.eval(1.5);
662 assert!((val - 3.0).abs() < 1e-8);
663 }
664 #[test]
665 fn test_bspline_fit_interpolates_when_num_ctrl_eq_data() {
666 let data: Vec<[f64; 3]> = (0..5)
667 .map(|i| {
668 let t = i as f64 / 4.0;
669 [t, t * t, 0.0]
670 })
671 .collect();
672 let curve = fit_bspline_least_squares(&data, 5, 3);
673 assert_eq!(curve.control_points.len(), 5);
674 }
675 #[test]
676 fn test_bspline_fit_approximates_data() {
677 let data: Vec<[f64; 3]> = (0..10)
678 .map(|i| {
679 let t = i as f64 / 9.0;
680 [t, (2.0 * PI * t).sin(), 0.0]
681 })
682 .collect();
683 let curve = fit_bspline_least_squares(&data, 6, 3);
684 let pt_mid = curve.eval(0.5);
685 assert!(pt_mid[0].abs() < 2.0, "mid x = {}", pt_mid[0]);
686 }
687 #[test]
688 fn test_surface_of_revolution_cylinder() {
689 let profile = vec![[1.0, 0.0], [1.0, 1.0]];
690 let grid = surface_of_revolution(&profile, 8);
691 assert_eq!(grid.len(), 2);
692 assert_eq!(grid[0].len(), 8);
693 for row in &grid {
694 for p in row {
695 let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
696 assert!((r - 1.0).abs() < 1e-9);
697 }
698 }
699 }
700 #[test]
701 fn test_surface_of_revolution_point_count() {
702 let profile: Vec<[f64; 2]> = (0..5).map(|i| [i as f64, i as f64]).collect();
703 let grid = surface_of_revolution(&profile, 12);
704 assert_eq!(grid.len(), 5);
705 for row in &grid {
706 assert_eq!(row.len(), 12);
707 }
708 }
709 #[test]
710 fn test_swept_surface_grid_shape() {
711 let spine: Vec<[f64; 3]> = (0..5).map(|i| [i as f64, 0.0, 0.0]).collect();
712 let profile = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
713 let sw = SweptSurface::new(spine, profile);
714 let grid = sw.compute();
715 assert_eq!(grid.len(), 5);
716 assert_eq!(grid[0].len(), 3);
717 }
718 #[test]
719 fn test_swept_surface_spine_on_surface() {
720 let spine: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
721 let profile = vec![[0.0, 0.0]];
722 let sw = SweptSurface::new(spine.clone(), profile);
723 let grid = sw.compute();
724 for (i, sp) in spine.iter().enumerate() {
725 for k in 0..3 {
726 assert!((grid[i][0][k] - sp[k]).abs() < 1e-9);
727 }
728 }
729 }
730 #[test]
731 fn test_lofted_surface_shape() {
732 let s0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
733 let s1 = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [2.0, 0.0, 1.0]];
734 let grid = lofted_surface(&[s0, s1], 5);
735 assert_eq!(grid.len(), 5);
736 assert_eq!(grid[0].len(), 3);
737 }
738 #[test]
739 fn test_lofted_surface_endpoints() {
740 let s0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
741 let s1 = vec![[0.0, 0.0, 2.0], [1.0, 0.0, 2.0]];
742 let grid = lofted_surface(&[s0.clone(), s1.clone()], 3);
743 for k in 0..3 {
744 assert!((grid[0][0][k] - s0[0][k]).abs() < 1e-9);
745 assert!((grid[2][0][k] - s1[0][k]).abs() < 1e-9);
746 }
747 }
748 #[test]
749 fn test_blending_surface_g0_endpoints() {
750 let c0 = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
751 let c1 = BezierCurve::new(vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]);
752 let blend = BlendingSurface::new(c0.clone(), c1.clone(), 1.0, 1.0, ContinuityOrder::G0);
753 let p0 = blend.eval(0.0, 0.5);
754 let p1 = blend.eval(1.0, 0.5);
755 let ep0 = c0.eval(0.5);
756 let ep1 = c1.eval(0.5);
757 for k in 0..3 {
758 assert!((p0[k] - ep0[k]).abs() < 1e-9);
759 assert!((p1[k] - ep1[k]).abs() < 1e-9);
760 }
761 }
762 #[test]
763 fn test_blending_surface_g1_continuity() {
764 let c0 = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
765 let c1 = BezierCurve::new(vec![[0.0, 2.0, 0.0], [1.0, 2.0, 0.0], [2.0, 2.0, 0.0]]);
766 let blend = BlendingSurface::new(c0, c1, 0.5, 0.5, ContinuityOrder::G1);
767 let grid = blend.sample_grid(5, 5);
768 assert_eq!(grid.len(), 5);
769 assert_eq!(grid[0].len(), 5);
770 }
771 #[test]
772 fn test_blending_surface_g2_sample() {
773 let c0 = BezierCurve::new(vec![
774 [0.0, 0.0, 0.0],
775 [1.0, 0.0, 0.0],
776 [2.0, 0.0, 0.0],
777 [3.0, 0.0, 0.0],
778 ]);
779 let c1 = BezierCurve::new(vec![
780 [0.0, 3.0, 0.0],
781 [1.0, 3.0, 0.0],
782 [2.0, 3.0, 0.0],
783 [3.0, 3.0, 0.0],
784 ]);
785 let blend = BlendingSurface::new(c0, c1, 0.3, 0.3, ContinuityOrder::G2);
786 let p = blend.eval(0.5, 0.5);
787 assert!(p[1] > 0.0 && p[1] < 3.0);
788 }
789 #[test]
790 fn test_curvature_line_is_zero() {
791 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
792 let curve = BezierCurve::new(ctrl);
793 let kappa = curve_curvature(&curve, 0.5);
794 assert!(kappa < 1e-8, "curvature of line = {}", kappa);
795 }
796 #[test]
797 fn test_curvature_circle_approx() {
798 let r = 2.0;
799 let ctrl = vec![[r, 0.0, 0.0], [r, r, 0.0], [0.0, r, 0.0]];
800 let curve = BezierCurve::new(ctrl);
801 let kappa = curve_curvature(&curve, 0.5);
802 assert!(kappa > 0.0);
803 }
804 #[test]
805 fn test_frenet_serret_orthonormal() {
806 let ctrl = vec![
807 [0.0, 0.0, 0.0],
808 [1.0, 1.0, 0.0],
809 [2.0, 0.0, 1.0],
810 [3.0, 1.0, 1.0],
811 ];
812 let curve = BezierCurve::new(ctrl);
813 let (t, n, b) = frenet_serret_frame(&curve, 0.5);
814 assert!((vec3_norm(t) - 1.0).abs() < 1e-6);
815 let _ = n;
816 let _ = b;
817 }
818 #[test]
819 fn test_osculating_radius_line_is_infinity() {
820 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
821 let curve = BezierCurve::new(ctrl);
822 let r = osculating_radius(&curve, 0.5);
823 assert!(r == f64::INFINITY || r > 1e10);
824 }
825 #[test]
826 fn test_bspline_surface_eval_shape() {
827 let net: Vec<Vec<[f64; 3]>> = (0..3)
828 .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
829 .collect();
830 let surf = BSplineSurface::with_clamped_knots(net, 2, 2);
831 let p = surf.eval(0.5, 0.5);
832 assert!(p[0] >= 0.0 && p[0] <= 2.0);
833 assert!(p[1] >= 0.0 && p[1] <= 2.0);
834 }
835 #[test]
836 fn test_bspline_surface_normal_nonzero() {
837 let net: Vec<Vec<[f64; 3]>> = (0..3)
838 .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
839 .collect();
840 let surf = BSplineSurface::with_clamped_knots(net, 2, 2);
841 let n = surf.normal(0.5, 0.5);
842 let norm = vec3_norm(n);
843 assert!((norm - 1.0).abs() < 0.1, "normal norm = {}", norm);
844 }
845 #[test]
846 fn test_bspline_surface_sample_grid_dimensions() {
847 let net: Vec<Vec<[f64; 3]>> = (0..4)
848 .map(|i| (0..4).map(|j| [i as f64, j as f64, 0.0]).collect())
849 .collect();
850 let surf = BSplineSurface::with_clamped_knots(net, 3, 3);
851 let grid = surf.sample_grid(5, 7);
852 assert_eq!(grid.len(), 5);
853 assert_eq!(grid[0].len(), 7);
854 }
855 #[test]
856 fn test_nurbs_surface_eval() {
857 let net: Vec<Vec<[f64; 4]>> = (0..3)
858 .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0, 1.0]).collect())
859 .collect();
860 let ku = uniform_clamped_knots(3, 2);
861 let kv = uniform_clamped_knots(3, 2);
862 let surf = NurbsSurface::new(net, 2, 2, ku, kv);
863 let p = surf.eval(0.5, 0.5);
864 assert!(p[0].is_finite() && p[1].is_finite());
865 }
866 #[test]
867 fn test_periodic_bspline_returns_finite() {
868 let ctrl = vec![
869 [0.0, 0.0, 0.0],
870 [1.0, 0.0, 0.0],
871 [1.0, 1.0, 0.0],
872 [0.0, 1.0, 0.0],
873 ];
874 let curve = PeriodicBSpline::new(ctrl, 3);
875 let p = curve.eval(0.5);
876 for &pk in p.iter() {
877 assert!(pk.is_finite());
878 }
879 }
880 #[test]
881 fn test_chord_length_params_bounds() {
882 let pts = vec![
883 [0.0, 0.0, 0.0],
884 [1.0, 0.0, 0.0],
885 [2.0, 1.0, 0.0],
886 [3.0, 0.0, 0.0],
887 ];
888 let params = chord_length_params(&pts);
889 assert_eq!(params.len(), 4);
890 assert!((params[0] - 0.0).abs() < EPS);
891 assert!((params[3] - 1.0).abs() < EPS);
892 for i in 0..params.len() - 1 {
893 assert!(params[i] <= params[i + 1]);
894 }
895 }
896 #[test]
897 fn test_knot_insert_preserves_shape() {
898 let ctrl = vec![
899 [0.0, 0.0, 0.0],
900 [1.0, 1.0, 0.0],
901 [2.0, 0.0, 0.0],
902 [3.0, 1.0, 0.0],
903 ];
904 let curve = BSplineCurve::with_clamped_knots(ctrl, 3);
905 let refined = knot_insert(&curve, 0.5);
906 for i in 1..9 {
907 let t = i as f64 / 9.0;
908 let p_orig = curve.eval(t);
909 let p_ref = refined.eval(t);
910 for k in 0..3 {
911 assert!(
912 (p_orig[k] - p_ref[k]).abs() < 1e-8,
913 "knot insert changed shape at t={}: {} vs {}",
914 t,
915 p_orig[k],
916 p_ref[k]
917 );
918 }
919 }
920 }
921 #[test]
922 fn test_knot_insert_increases_knot_count() {
923 let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
924 let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
925 let n_before = curve.knots.len();
926 let refined = knot_insert(&curve, 0.4);
927 assert_eq!(refined.knots.len(), n_before + 1);
928 }
929 #[test]
930 fn test_bezier_patch_corner_interpolation() {
931 let grid: Vec<Vec<[f64; 3]>> = (0..3)
932 .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
933 .collect();
934 let patch = BezierPatch::new(grid.clone());
935 let p00 = patch.eval(0.0, 0.0);
936 let p11 = patch.eval(1.0, 1.0);
937 assert!((p00[0] - grid[0][0][0]).abs() < EPS);
938 assert!((p11[0] - grid[2][2][0]).abs() < EPS);
939 }
940 #[test]
941 fn test_bezier_patch_sample_grid_shape() {
942 let grid: Vec<Vec<[f64; 3]>> = (0..4)
943 .map(|i| (0..4).map(|j| [i as f64, j as f64, 0.0]).collect())
944 .collect();
945 let patch = BezierPatch::new(grid);
946 let sampled = patch.sample_grid(6, 8);
947 assert_eq!(sampled.len(), 6);
948 assert_eq!(sampled[0].len(), 8);
949 }
950 #[test]
951 fn test_vec3_cross_orthogonal() {
952 let a = [1.0, 0.0, 0.0];
953 let b = [0.0, 1.0, 0.0];
954 let c = vec3_cross(a, b);
955 assert!((c[0]).abs() < EPS);
956 assert!((c[1]).abs() < EPS);
957 assert!((c[2] - 1.0).abs() < EPS);
958 }
959 #[test]
960 fn test_vec3_normalize_unit() {
961 let a = [3.0, 4.0, 0.0];
962 let n = vec3_normalize(a);
963 assert!((vec3_norm(n) - 1.0).abs() < 1e-9);
964 }
965 #[test]
966 fn test_vec3_lerp_midpoint() {
967 let a = [0.0, 0.0, 0.0];
968 let b = [2.0, 4.0, 6.0];
969 let mid = vec3_lerp(a, b, 0.5);
970 assert!((mid[0] - 1.0).abs() < EPS);
971 assert!((mid[1] - 2.0).abs() < EPS);
972 assert!((mid[2] - 3.0).abs() < EPS);
973 }
974}