1use super::types::BezierCurve;
6
7#[inline]
8pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
9 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
10}
11#[inline]
12pub(super) fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
13 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
14}
15#[inline]
16pub(super) fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
17 [a[0] * s, a[1] * s, a[2] * s]
18}
19#[inline]
20pub(super) fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
21 add3(scale3(a, 1.0 - t), scale3(b, t))
22}
23#[inline]
24pub(super) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25 [
26 a[1] * b[2] - a[2] * b[1],
27 a[2] * b[0] - a[0] * b[2],
28 a[0] * b[1] - a[1] * b[0],
29 ]
30}
31#[inline]
32pub(super) fn normalize3(a: [f64; 3]) -> [f64; 3] {
33 let len = (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt();
34 if len < 1e-12 {
35 [0.0, 0.0, 0.0]
36 } else {
37 scale3(a, 1.0 / len)
38 }
39}
40#[inline]
41pub(super) fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
42 let d = sub3(a, b);
43 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
44}
45#[inline]
46pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
47 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
48}
49pub(super) fn arbitrary_perp(v: [f64; 3]) -> [f64; 3] {
51 let candidates: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
52 for &c in &candidates {
53 let proj = dot3(v, c);
54 let perp = sub3(c, scale3(v, proj));
55 let len = dot3(perp, perp).sqrt();
56 if len > 1e-8 {
57 return scale3(perp, 1.0 / len);
58 }
59 }
60 [1.0, 0.0, 0.0]
61}
62pub fn bezier_arc_approximation(
67 center: [f64; 3],
68 radius: f64,
69 start_angle: f64,
70 end_angle: f64,
71 n_segments: usize,
72) -> Vec<[f64; 3]> {
73 if n_segments == 0 {
74 return Vec::new();
75 }
76 let total_angle = end_angle - start_angle;
77 let seg_angle = total_angle / n_segments as f64;
78 let mut pts = Vec::with_capacity(n_segments * 2 + 1);
79 for k in 0..n_segments {
80 let a0 = start_angle + k as f64 * seg_angle;
81 let a1 = a0 + seg_angle;
82 let a_mid = (a0 + a1) / 2.0;
83 let p0 = [
84 center[0] + radius * a0.cos(),
85 center[1] + radius * a0.sin(),
86 center[2],
87 ];
88 let r_mid = radius / a_mid.cos().max(1e-12) * (seg_angle / 2.0).cos();
89 let _ = r_mid;
90 let t = (seg_angle / 2.0).tan();
91 let p1 = [
92 center[0] + radius * a_mid.cos() - radius * t * a_mid.sin(),
93 center[1] + radius * a_mid.sin() + radius * t * a_mid.cos(),
94 center[2],
95 ];
96 if k == 0 {
97 pts.push(p0);
98 }
99 pts.push(p1);
100 let p2 = [
101 center[0] + radius * a1.cos(),
102 center[1] + radius * a1.sin(),
103 center[2],
104 ];
105 pts.push(p2);
106 }
107 pts
108}
109pub fn sample_curve_uniform(eval: &dyn Fn(f64) -> [f64; 3], n: usize) -> Vec<[f64; 3]> {
115 if n == 0 {
116 return Vec::new();
117 }
118 if n == 1 {
119 return vec![eval(0.0)];
120 }
121 (0..n).map(|i| eval(i as f64 / (n - 1) as f64)).collect()
122}
123pub fn bezier_curvature(curve: &BezierCurve, t: f64) -> f64 {
127 let eps = 1e-5_f64;
128 let d1 = curve.derivative(t);
129 let t0 = (t - eps).max(0.0);
130 let t1 = (t + eps).min(1.0);
131 let d1_0 = curve.derivative(t0);
132 let d1_1 = curve.derivative(t1);
133 let d2 = scale3(sub3(d1_1, d1_0), 1.0 / (t1 - t0));
134 let cross_mag = {
135 let c = cross3(d1, d2);
136 (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt()
137 };
138 let d1_mag = (d1[0] * d1[0] + d1[1] * d1[1] + d1[2] * d1[2]).sqrt();
139 if d1_mag < 1e-12 {
140 return 0.0;
141 }
142 cross_mag / (d1_mag * d1_mag * d1_mag)
143}
144pub fn bezier_torsion(curve: &BezierCurve, t: f64) -> f64 {
148 let eps = 1e-5_f64;
149 let d1 = curve.derivative(t);
150 let t0 = (t - eps).max(0.0);
151 let t1 = (t + eps).min(1.0);
152 let d1_0 = curve.derivative(t0);
153 let d1_1 = curve.derivative(t1);
154 let d2 = scale3(sub3(d1_1, d1_0), 1.0 / (t1 - t0));
155 let t00 = (t - 2.0 * eps).max(0.0);
156 let t11 = (t + 2.0 * eps).min(1.0);
157 let d1_00 = curve.derivative(t00);
158 let d1_11 = curve.derivative(t11);
159 let d2_0 = scale3(sub3(d1, d1_00), 1.0 / eps);
160 let d2_1 = scale3(sub3(d1_11, d1), 1.0 / eps);
161 let d3 = scale3(sub3(d2_1, d2_0), 1.0 / (2.0 * eps));
162 let cross = cross3(d1, d2);
163 let cross_mag2 = cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2];
164 if cross_mag2 < 1e-20 {
165 return 0.0;
166 }
167 let dot_val = cross[0] * d3[0] + cross[1] * d3[1] + cross[2] * d3[2];
168 dot_val / cross_mag2
169}
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::bspline::BsplineSurface;
174 use crate::bspline::KnotVector;
175 use crate::bspline::NurbsCurve;
176 use crate::bspline::NurbsSurface;
177 use crate::parametric::Arc;
178 use crate::parametric::BSpline;
179 use crate::parametric::BezierSurface;
180 use crate::parametric::CatmullRomSpline;
181 use crate::parametric::FrenetFrame;
182 use crate::parametric::HermiteCurve;
183 use crate::parametric::LoftSurface;
184 use crate::parametric::RevolutionSurface;
185 use crate::parametric::SweptSurface;
186 use crate::parametric::TangentFrame;
187 use crate::parametric::TubeGeometry;
188 #[test]
189 fn bezier_degree1_endpoints() {
190 let p0 = [0.0, 0.0, 0.0];
191 let p1 = [4.0, 2.0, 0.0];
192 let curve = BezierCurve::new(vec![p0, p1]);
193 let at0 = curve.evaluate(0.0);
194 let at1 = curve.evaluate(1.0);
195 let at_half = curve.evaluate(0.5);
196 for k in 0..3 {
197 assert!((at0[k] - p0[k]).abs() < 1e-10);
198 assert!((at1[k] - p1[k]).abs() < 1e-10);
199 assert!((at_half[k] - (p0[k] + p1[k]) / 2.0).abs() < 1e-10);
200 }
201 }
202 #[test]
203 fn bezier_degree2_midpoint() {
204 let p0 = [0.0, 0.0, 0.0];
205 let p1 = [1.0, 2.0, 0.0];
206 let p2 = [2.0, 0.0, 0.0];
207 let curve = BezierCurve::new(vec![p0, p1, p2]);
208 let expected = [1.0, 1.0, 0.0];
209 let result = curve.evaluate(0.5);
210 for k in 0..3 {
211 assert!(
212 (result[k] - expected[k]).abs() < 1e-10,
213 "component {k}: got {}",
214 result[k]
215 );
216 }
217 }
218 #[test]
219 fn bezier_arc_length_line() {
220 let curve = BezierCurve::new(vec![[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]]);
221 let len = curve.arc_length(1000);
222 assert!((len - 3.0).abs() < 1e-6, "expected ≈3.0, got {len}");
223 }
224 #[test]
225 fn bezier_surface_flat_normal() {
226 let grid: Vec<Vec<[f64; 3]>> = (0..4)
227 .map(|i| {
228 (0..4)
229 .map(|j| [i as f64 / 3.0, j as f64 / 3.0, 0.0])
230 .collect()
231 })
232 .collect();
233 let surf = BezierSurface::new_bicubic(grid);
234 let n = surf.normal(0.5, 0.5);
235 assert!(n[0].abs() < 1e-4, "nx={}", n[0]);
236 assert!(n[1].abs() < 1e-4, "ny={}", n[1]);
237 assert!((n[2].abs() - 1.0).abs() < 1e-4, "nz={}", n[2]);
238 }
239 #[test]
240 fn bspline_clamped_endpoints() {
241 let pts = vec![
242 [0.0, 0.0, 0.0],
243 [1.0, 2.0, 0.0],
244 [3.0, 0.0, 1.0],
245 [4.0, 1.0, 0.0],
246 ];
247 let spline = BSpline::clamped_uniform(3, pts.clone());
248 let at0 = spline.eval(0.0);
249 let at1 = spline.eval(1.0);
250 for k in 0..3 {
251 assert!(
252 (at0[k] - pts[0][k]).abs() < 1e-10,
253 "start: axis {k}: {}",
254 at0[k]
255 );
256 assert!(
257 (at1[k] - pts[pts.len() - 1][k]).abs() < 1e-10,
258 "end: axis {k}: {}",
259 at1[k]
260 );
261 }
262 }
263 #[test]
264 fn bspline_knot_insert_preserves_curve() {
265 let pts = vec![
266 [0.0, 0.0, 0.0],
267 [1.0, 1.0, 0.0],
268 [2.0, 0.0, 0.0],
269 [3.0, 1.0, 0.0],
270 ];
271 let spline = BSpline::clamped_uniform(2, pts);
272 let new_spline = spline.insert_knot(0.5);
273 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
274 let p_orig = spline.eval(t);
275 let p_new = new_spline.eval(t);
276 for k in 0..3 {
277 assert!(
278 (p_orig[k] - p_new[k]).abs() < 1e-9,
279 "t={t} axis {k}: orig={} new={}",
280 p_orig[k],
281 p_new[k]
282 );
283 }
284 }
285 }
286 #[test]
287 fn bspline_degree1_is_polyline() {
288 let pts = vec![
289 [0.0, 0.0, 0.0],
290 [1.0, 0.0, 0.0],
291 [1.0, 1.0, 0.0],
292 [2.0, 1.0, 0.0],
293 ];
294 let spline = BSpline::clamped_uniform(1, pts.clone());
295 let p0 = spline.eval(0.0);
296 let p1 = spline.eval(1.0);
297 for k in 0..3 {
298 assert!((p0[k] - pts[0][k]).abs() < 1e-9);
299 assert!((p1[k] - pts[pts.len() - 1][k]).abs() < 1e-9);
300 }
301 }
302 #[test]
303 fn hermite_interpolates_endpoints() {
304 let pts = vec![[0.0, 0.0, 0.0], [2.0, 3.0, 0.0], [4.0, 0.0, 0.0]];
305 let tgts = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]];
306 let curve = HermiteCurve::new(pts.clone(), tgts);
307 for (i, &pt) in pts.iter().enumerate() {
308 let evaled = curve.eval(i as f64);
309 for k in 0..3 {
310 assert!(
311 (evaled[k] - pt[k]).abs() < 1e-10,
312 "seg {i} axis {k}: {}",
313 evaled[k]
314 );
315 }
316 }
317 }
318 #[test]
319 fn hermite_c0_continuity() {
320 let pts = vec![
321 [0.0, 0.0, 0.0],
322 [1.0, 1.0, 0.0],
323 [2.0, 0.0, 0.0],
324 [3.0, 1.0, 0.0],
325 ];
326 let tgts = vec![
327 [0.5, 0.5, 0.0],
328 [0.5, -0.5, 0.0],
329 [0.5, 0.5, 0.0],
330 [0.5, -0.5, 0.0],
331 ];
332 let curve = HermiteCurve::new(pts, tgts);
333 for knot in [1.0, 2.0] {
334 let eps = 1e-8;
335 let left = curve.eval(knot - eps);
336 let right = curve.eval(knot + eps);
337 for k in 0..3 {
338 assert!((left[k] - right[k]).abs() < 1e-5, "C0 at {knot} axis {k}");
339 }
340 }
341 }
342 #[test]
343 fn hermite_derivative_at_knot() {
344 let pts = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
345 let tgts = vec![[3.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
346 let curve = HermiteCurve::new(pts, tgts.clone());
347 let deriv = curve.derivative(0.0);
348 for k in 0..3 {
349 assert!(
350 (deriv[k] - tgts[0][k]).abs() < 1e-10,
351 "axis {k}: {}",
352 deriv[k]
353 );
354 }
355 }
356 #[test]
357 fn catmull_rom_interpolates_interior() {
358 let pts = vec![
359 [0.0, 0.0, 0.0],
360 [1.0, 1.0, 0.0],
361 [2.0, 0.0, 0.0],
362 [3.0, 1.0, 0.0],
363 ];
364 let spline = CatmullRomSpline::new(pts.clone());
365 for (i, &pt) in pts.iter().enumerate() {
366 let t = spline.knots[i];
367 let evaled = spline.eval(t);
368 for k in 0..3 {
369 assert!(
370 (evaled[k] - pt[k]).abs() < 1e-6,
371 "point {i} axis {k}: got {} expected {}",
372 evaled[k],
373 pt[k]
374 );
375 }
376 }
377 }
378 #[test]
379 fn catmull_rom_range_clamp() {
380 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
381 let spline = CatmullRomSpline::new(pts);
382 let p_neg = spline.eval(-0.1);
383 let p_over = spline.eval(1.5);
384 for k in 0..3 {
385 assert!(p_neg[k].is_finite());
386 assert!(p_over[k].is_finite());
387 }
388 }
389 #[test]
390 fn frenet_tangent_is_unit() {
391 let circle_pts: Vec<[f64; 3]> = (0..=20)
392 .map(|i| {
393 let t = i as f64 / 20.0 * std::f64::consts::TAU;
394 [t.cos(), t.sin(), 0.0]
395 })
396 .collect();
397 let curve = BezierCurve::new(circle_pts.clone());
398 let frame = FrenetFrame::from_bezier(&curve, 0.5);
399 let len =
400 (frame.tangent[0].powi(2) + frame.tangent[1].powi(2) + frame.tangent[2].powi(2)).sqrt();
401 assert!((len - 1.0).abs() < 1e-6, "tangent not unit: len={len}");
402 }
403 #[test]
404 fn frenet_triad_orthonormal() {
405 let pts = vec![
406 [0.0, 0.0, 0.0],
407 [1.0, 1.0, 0.0],
408 [2.0, 0.0, 1.0],
409 [3.0, 0.0, 0.0],
410 ];
411 let curve = BezierCurve::new(pts);
412 let frame = FrenetFrame::from_bezier(&curve, 0.5);
413 let t = frame.tangent;
414 let n = frame.normal;
415 let b = frame.binormal;
416 assert!(dot3(t, n).abs() < 1e-6, "T·N = {}", dot3(t, n));
417 assert!(dot3(t, b).abs() < 1e-6, "T·B = {}", dot3(t, b));
418 assert!(dot3(n, b).abs() < 1e-6, "N·B = {}", dot3(n, b));
419 for (name, v) in [("T", t), ("N", n), ("B", b)] {
420 let len = (v[0].powi(2) + v[1].powi(2) + v[2].powi(2)).sqrt();
421 assert!((len - 1.0).abs() < 1e-6, "{name} not unit: {len}");
422 }
423 }
424 #[test]
425 fn tube_vertex_count() {
426 let curve = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
427 let tube = TubeGeometry::from_bezier(&curve, 0.1, 8, 12);
428 assert_eq!(tube.vertices.len(), 8 * 12, "vertex count");
429 assert_eq!(tube.triangles.len(), (8 - 1) * 12 * 2, "triangle count");
430 }
431 #[test]
432 fn tube_vertices_near_spine() {
433 let pts = vec![[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
434 let curve = BezierCurve::new(pts);
435 let radius = 0.5;
436 let tube = TubeGeometry::from_bezier(&curve, radius, 10, 8);
437 for v in &tube.vertices {
438 let r = (v[1].powi(2) + v[2].powi(2)).sqrt();
439 assert!(
440 (r - radius).abs() < 1e-8,
441 "vertex [{},{},{}] radius {r} != {radius}",
442 v[0],
443 v[1],
444 v[2]
445 );
446 }
447 }
448 #[test]
449 fn bezier_derivative_line() {
450 let curve = BezierCurve::new(vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
451 for &t in &[0.0, 0.5, 1.0] {
452 let d = curve.derivative(t);
453 assert!((d[0] - 2.0).abs() < 1e-10, "dx at t={t}: {}", d[0]);
454 assert!(d[1].abs() < 1e-10);
455 assert!(d[2].abs() < 1e-10);
456 }
457 }
458 #[test]
459 fn bezier_eval_at_t0_returns_first_control_point() {
460 let p0 = [1.0, 2.0, 3.0];
461 let p1 = [4.0, 5.0, 6.0];
462 let p2 = [7.0, 8.0, 9.0];
463 let curve = BezierCurve::new(vec![p0, p1, p2]);
464 let result = curve.eval(0.0);
465 for k in 0..3 {
466 assert!(
467 (result[k] - p0[k]).abs() < 1e-10,
468 "axis {k}: {} != {}",
469 result[k],
470 p0[k]
471 );
472 }
473 }
474 #[test]
475 fn bezier_eval_at_t1_returns_last_control_point() {
476 let p0 = [1.0, 2.0, 3.0];
477 let p1 = [4.0, 5.0, 6.0];
478 let p2 = [7.0, 8.0, 9.0];
479 let curve = BezierCurve::new(vec![p0, p1, p2]);
480 let result = curve.eval(1.0);
481 for k in 0..3 {
482 assert!(
483 (result[k] - p2[k]).abs() < 1e-10,
484 "axis {k}: {} != {}",
485 result[k],
486 p2[k]
487 );
488 }
489 }
490 #[test]
491 fn bspline_order_clamped() {
492 let pts = vec![
493 [0.0, 0.0, 0.0],
494 [1.0, 2.0, 0.0],
495 [3.0, 0.0, 1.0],
496 [4.0, 1.0, 0.0],
497 ];
498 let spline = BSpline::clamped_uniform(3, pts.clone());
499 let start = spline.eval(0.0);
500 let end = spline.eval(1.0);
501 for k in 0..3 {
502 assert!(
503 (start[k] - pts[0][k]).abs() < 1e-9,
504 "start axis {k}: got {} expected {}",
505 start[k],
506 pts[0][k]
507 );
508 assert!(
509 (end[k] - pts[pts.len() - 1][k]).abs() < 1e-9,
510 "end axis {k}: got {} expected {}",
511 end[k],
512 pts[pts.len() - 1][k]
513 );
514 }
515 }
516 #[test]
517 fn nurbs_surface_eval_non_nan() {
518 let mut control_points: Vec<Vec<[f64; 3]>> = Vec::new();
519 let mut weights: Vec<Vec<f64>> = Vec::new();
520 for i in 0..3 {
521 let mut cp_row = Vec::new();
522 let mut w_row = Vec::new();
523 for j in 0..3 {
524 cp_row.push([i as f64, j as f64, 0.0]);
525 w_row.push(1.0);
526 }
527 control_points.push(cp_row);
528 weights.push(w_row);
529 }
530 let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
531 let surf = NurbsSurface::new(
532 2,
533 2,
534 KnotVector::new(knots.clone()),
535 KnotVector::new(knots),
536 control_points,
537 weights,
538 );
539 for &u in &[0.0, 0.5, 1.0] {
540 for &v in &[0.0, 0.5, 1.0] {
541 let p = surf.eval(u, v);
542 for &val in &p {
543 assert!(val.is_finite(), "NurbsSurface eval({u},{v}) returned NaN");
544 }
545 }
546 }
547 }
548 #[test]
549 fn bezier_arc_approximation_points_on_circle() {
550 let center = [0.0, 0.0, 0.0];
551 let radius = 2.0;
552 let pts = bezier_arc_approximation(center, radius, 0.0, std::f64::consts::PI, 4);
553 assert!(!pts.is_empty(), "arc approximation should return points");
554 let first = pts[0];
555 let last = *pts.last().unwrap();
556 let r0 = (first[0].powi(2) + first[1].powi(2)).sqrt();
557 let r1 = (last[0].powi(2) + last[1].powi(2)).sqrt();
558 assert!(
559 (r0 - radius).abs() < 1e-9,
560 "first point not on circle: r={r0}"
561 );
562 assert!(
563 (r1 - radius).abs() < 1e-9,
564 "last point not on circle: r={r1}"
565 );
566 }
567 #[test]
568 fn sample_curve_uniform_count() {
569 let n = 10usize;
570 let pts = sample_curve_uniform(&|t| [t, t * 2.0, 0.0], n);
571 assert_eq!(pts.len(), n);
572 assert!((pts[0][0] - 0.0).abs() < 1e-10);
573 assert!((pts[n - 1][0] - 1.0).abs() < 1e-10);
574 }
575 #[test]
576 fn nurbs_basis_partition_of_unity() {
577 let degree = 3_usize;
578 let knots = vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0];
579 let n_ctrl = 5_usize;
580 for &t in &[0.0, 0.25, 0.5, 0.75] {
581 let sum: f64 = (0..n_ctrl)
582 .map(|i| NurbsCurve::b_spline_basis(i, degree, t, &knots))
583 .sum();
584 assert!(
585 (sum - 1.0).abs() < 1e-10,
586 "partition of unity failed at t={t}: sum={sum}"
587 );
588 }
589 let sum_at_1: f64 = (0..n_ctrl)
590 .map(|i| NurbsCurve::b_spline_basis(i, degree, 1.0, &knots))
591 .sum();
592 assert!(
593 (sum_at_1 - 1.0).abs() < 1e-10,
594 "partition of unity failed at t=1.0: sum={sum_at_1}"
595 );
596 }
597 #[test]
598 fn arc_endpoints_on_circle() {
599 let center = [1.0, 2.0, 0.0];
600 let radius = 3.0;
601 let arc = Arc::in_xy_plane(center, radius, 0.0, std::f64::consts::PI);
602 let p0 = arc.evaluate(0.0);
603 let p1 = arc.evaluate(1.0);
604 let r0 = dist3(p0, center);
605 let r1 = dist3(p1, center);
606 assert!((r0 - radius).abs() < 1e-9, "start not on circle: r={r0}");
607 assert!((r1 - radius).abs() < 1e-9, "end not on circle: r={r1}");
608 }
609 #[test]
610 fn arc_length_half_circle() {
611 let radius = 2.0;
612 let arc = Arc::in_xy_plane([0.0; 3], radius, 0.0, std::f64::consts::PI);
613 let expected = std::f64::consts::PI * radius;
614 let len = arc.arc_length();
615 assert!(
616 (len - expected).abs() < 1e-9,
617 "expected {expected}, got {len}"
618 );
619 }
620 #[test]
621 fn arc_sample_count() {
622 let arc = Arc::in_xy_plane([0.0; 3], 1.0, 0.0, std::f64::consts::TAU);
623 let pts = arc.sample(10);
624 assert_eq!(pts.len(), 10);
625 }
626 #[test]
627 fn arc_tangent_perpendicular_to_radius() {
628 let center = [0.0; 3];
629 let arc = Arc::in_xy_plane(center, 1.0, 0.0, std::f64::consts::TAU);
630 for i in 0..4 {
631 let t = i as f64 / 4.0;
632 let p = arc.evaluate(t);
633 let tan = arc.tangent(t);
634 let radial = sub3(p, center);
635 let d = dot3(radial, tan);
636 assert!(
637 d.abs() < 1e-6,
638 "tangent not perpendicular to radius at t={t}: dot={d}"
639 );
640 }
641 }
642 #[test]
643 fn loft_surface_at_v0_is_curve_a() {
644 let ca = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
645 let cb = BezierCurve::new(vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]);
646 let loft = LoftSurface::new(ca, cb);
647 for &u in &[0.0, 0.5, 1.0] {
648 let p = loft.evaluate(u, 0.0);
649 let expected = loft.curve_a.evaluate(u);
650 for k in 0..3 {
651 assert!(
652 (p[k] - expected[k]).abs() < 1e-9,
653 "v=0 loft should equal curve_a"
654 );
655 }
656 }
657 }
658 #[test]
659 fn loft_surface_at_v1_is_curve_b() {
660 let ca = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
661 let cb = BezierCurve::new(vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]);
662 let loft = LoftSurface::new(ca, cb);
663 for &u in &[0.0, 0.5, 1.0] {
664 let p = loft.evaluate(u, 1.0);
665 let expected = loft.curve_b.evaluate(u);
666 for k in 0..3 {
667 assert!(
668 (p[k] - expected[k]).abs() < 1e-9,
669 "v=1 loft should equal curve_b"
670 );
671 }
672 }
673 }
674 #[test]
675 fn loft_surface_normal_finite() {
676 let ca = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
677 let cb = BezierCurve::new(vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]);
678 let loft = LoftSurface::new(ca, cb);
679 let n = loft.normal(0.5, 0.5);
680 for &v in &n {
681 assert!(v.is_finite(), "loft normal must be finite: {v}");
682 }
683 }
684 #[test]
685 fn loft_sample_grid_dimensions() {
686 let ca = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
687 let cb = BezierCurve::new(vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]);
688 let loft = LoftSurface::new(ca, cb);
689 let grid = loft.sample_grid(4, 5);
690 assert_eq!(grid.len(), 4);
691 assert_eq!(grid[0].len(), 5);
692 }
693 #[test]
694 fn swept_surface_at_u0_is_at_spine_start() {
695 let spine = BezierCurve::new(vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
696 let profile = BezierCurve::new(vec![[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
697 let swept = SweptSurface::new(spine, profile);
698 let p00 = swept.evaluate(0.0, 0.0);
699 for (k, &p00k) in p00.iter().enumerate() {
700 assert!(p00k.abs() < 1e-9, "p00[{k}]={}", p00k);
701 }
702 }
703 #[test]
704 fn swept_surface_normal_is_unit() {
705 let spine = BezierCurve::new(vec![[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]]);
706 let profile = BezierCurve::new(vec![[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
707 let swept = SweptSurface::new(spine, profile);
708 let n = swept.normal(0.5, 0.5);
709 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
710 assert!(
711 (len - 1.0).abs() < 1e-5 || len < 1e-9,
712 "normal not unit: len={len}"
713 );
714 }
715 #[test]
716 fn revolution_surface_full_circle_radius() {
717 let profile = BezierCurve::new(vec![[2.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
718 let rev = RevolutionSurface::new(profile);
719 for i in 0..8 {
720 let u = i as f64 / 8.0;
721 let p = rev.evaluate(u, 0.5);
722 let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
723 assert!((r - 2.0).abs() < 1e-9, "radius at u={u}: {r}");
724 }
725 }
726 #[test]
727 fn revolution_surface_sample_grid_dimensions() {
728 let profile = BezierCurve::new(vec![[1.0, 0.0, 0.0], [1.0, 0.0, 2.0]]);
729 let rev = RevolutionSurface::new(profile);
730 let grid = rev.sample_grid(6, 4);
731 assert_eq!(grid.len(), 6);
732 assert_eq!(grid[0].len(), 4);
733 }
734 #[test]
735 fn tangent_frame_bezier_surface_normal_unit() {
736 let grid: Vec<Vec<[f64; 3]>> = (0..4)
737 .map(|i| {
738 (0..4)
739 .map(|j| [i as f64 / 3.0, j as f64 / 3.0, 0.0])
740 .collect()
741 })
742 .collect();
743 let surf = BezierSurface::new_bicubic(grid);
744 let frame = TangentFrame::from_bezier_surface(&surf, 0.5, 0.5);
745 let n_len =
746 (frame.normal[0].powi(2) + frame.normal[1].powi(2) + frame.normal[2].powi(2)).sqrt();
747 assert!((n_len - 1.0).abs() < 1e-5, "normal not unit: len={n_len}");
748 }
749 #[test]
750 fn tangent_frame_loft_orthogonality() {
751 let ca = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
752 let cb = BezierCurve::new(vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]);
753 let loft = LoftSurface::new(ca, cb);
754 let frame = TangentFrame::from_loft_surface(&loft, 0.5, 0.5);
755 let d = dot3(frame.tangent_u, frame.tangent_v);
756 assert!(d.abs() < 1e-5, "tangent_u . tangent_v = {d} (should be ~0)");
757 }
758 #[test]
759 fn bspline_derivative_finite() {
760 let pts = vec![
761 [0.0, 0.0, 0.0],
762 [1.0, 1.0, 0.0],
763 [2.0, 0.0, 0.0],
764 [3.0, 1.0, 0.0],
765 ];
766 let spline = BSpline::clamped_uniform(3, pts);
767 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
768 let d = spline.derivative(t);
769 for (k, &dk) in d.iter().enumerate() {
770 assert!(
771 dk.is_finite(),
772 "derivative[{k}] not finite at t={t}: {}",
773 dk
774 );
775 }
776 }
777 }
778 #[test]
779 fn bspline_arc_length_line_approx() {
780 let pts = vec![
781 [0.0, 0.0, 0.0],
782 [1.0, 0.0, 0.0],
783 [2.0, 0.0, 0.0],
784 [4.0, 0.0, 0.0],
785 ];
786 let spline = BSpline::clamped_uniform(1, pts);
787 let len = spline.arc_length(100);
788 assert!(
789 (len - 4.0).abs() < 1e-3,
790 "arc length should be ~4.0, got {len}"
791 );
792 }
793 #[test]
794 fn bezier_curvature_straight_line_is_zero() {
795 let curve = BezierCurve::new(vec![[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]]);
796 let kappa = bezier_curvature(&curve, 0.5);
797 assert!(
798 kappa < 1e-6,
799 "straight line curvature should be 0, got {kappa}"
800 );
801 }
802 #[test]
803 fn bezier_curvature_circle_approx_positive() {
804 let circle_pts: Vec<[f64; 3]> = (0..=8)
805 .map(|i| {
806 let t = i as f64 / 8.0 * std::f64::consts::TAU;
807 [t.cos(), t.sin(), 0.0]
808 })
809 .collect();
810 let curve = BezierCurve::new(circle_pts);
811 let kappa = bezier_curvature(&curve, 0.5);
812 assert!(
813 kappa.is_finite() && kappa >= 0.0,
814 "curvature should be non-negative: {kappa}"
815 );
816 }
817 #[test]
818 fn bezier_torsion_planar_curve_near_zero() {
819 let pts = vec![
820 [0.0, 0.0, 0.0],
821 [1.0, 1.0, 0.0],
822 [2.0, 0.0, 0.0],
823 [3.0, 1.0, 0.0],
824 ];
825 let curve = BezierCurve::new(pts);
826 let tau = bezier_torsion(&curve, 0.5);
827 assert!(tau.is_finite(), "torsion must be finite: {tau}");
828 assert!(
829 tau.abs() < 1.0,
830 "planar curve torsion should be small: {tau}"
831 );
832 }
833 fn flat_bspline_surface() -> BsplineSurface {
834 let net: Vec<Vec<[f64; 3]>> = (0..3)
835 .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
836 .collect();
837 let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
838 BsplineSurface::new(
839 2,
840 2,
841 KnotVector::new(knots.clone()),
842 KnotVector::new(knots),
843 net,
844 )
845 }
846 #[test]
847 fn bspline_surface_eval_finite() {
848 let surf = flat_bspline_surface();
849 for &u in &[0.0, 0.25, 0.5, 0.75, 1.0] {
850 for &v in &[0.0, 0.25, 0.5, 0.75, 1.0] {
851 let p = surf.eval(u, v);
852 for (k, &pk) in p.iter().enumerate() {
853 assert!(pk.is_finite(), "eval({u},{v})[{k}] not finite: {}", pk);
854 }
855 }
856 }
857 }
858 #[test]
859 fn bspline_surface_flat_gaussian_curvature_near_zero() {
860 let surf = flat_bspline_surface();
861 let (k_gauss, _k_mean) = surf.compute_curvature(0.5, 0.5);
862 assert!(
863 k_gauss.abs() < 1e-3,
864 "flat surface Gaussian curvature should be ~0, got {k_gauss}"
865 );
866 }
867 #[test]
868 fn bspline_surface_flat_mean_curvature_near_zero() {
869 let surf = flat_bspline_surface();
870 let (_k_gauss, k_mean) = surf.compute_curvature(0.5, 0.5);
871 assert!(
872 k_mean.abs() < 1e-3,
873 "flat surface mean curvature should be ~0, got {k_mean}"
874 );
875 }
876 #[test]
877 fn bspline_surface_curvature_returns_finite() {
878 let surf = flat_bspline_surface();
879 let (k_g, k_h) = surf.compute_curvature(0.3, 0.7);
880 assert!(
881 k_g.is_finite(),
882 "Gaussian curvature should be finite: {k_g}"
883 );
884 assert!(k_h.is_finite(), "mean curvature should be finite: {k_h}");
885 }
886 #[test]
887 fn bspline_surface_refine_knots_increases_control_points() {
888 let surf = flat_bspline_surface();
889 let refined = surf.refine_knots(&[0.5], &[0.5]);
890 let n_u_before = surf.control_points.len();
891 let n_u_after = refined.control_points.len();
892 assert!(
893 n_u_after >= n_u_before,
894 "refine_knots should not reduce control point count in u: {n_u_after} < {n_u_before}"
895 );
896 }
897 #[test]
898 fn bspline_surface_refine_knots_preserves_geometry() {
899 let surf = flat_bspline_surface();
900 let refined = surf.refine_knots(&[0.5], &[0.5]);
901 for &(u, v) in &[(0.25, 0.25), (0.5, 0.5), (0.75, 0.75)] {
902 let p_orig = surf.eval(u, v);
903 let p_refined = refined.eval(u, v);
904 for k in 0..3 {
905 assert!(
906 (p_orig[k] - p_refined[k]).abs() < 1e-6,
907 "eval changed after refinement at ({u},{v})[{k}]: {} vs {}",
908 p_orig[k],
909 p_refined[k]
910 );
911 }
912 }
913 }
914 fn simple_nurbs_line() -> NurbsCurve {
915 NurbsCurve::new(
916 1,
917 KnotVector::new(vec![0.0, 0.0, 1.0, 1.0]),
918 vec![[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]],
919 vec![1.0, 1.0],
920 )
921 }
922 #[test]
923 fn nurbs_arc_length_line() {
924 let curve = simple_nurbs_line();
925 let len = curve.arc_length(0.0, 1.0, 200);
926 assert!(
927 (len - 4.0).abs() < 1e-3,
928 "NURBS line arc length should be ~4.0, got {len}"
929 );
930 }
931 #[test]
932 fn nurbs_arc_length_reparametrize_count() {
933 let curve = simple_nurbs_line();
934 let pts = curve.compute_arc_length_reparametrize(200, 10);
935 assert_eq!(
936 pts.len(),
937 10,
938 "should return exactly 10 reparametrized points"
939 );
940 }
941 #[test]
942 fn nurbs_arc_length_reparametrize_uniform_spacing() {
943 let curve = simple_nurbs_line();
944 let pts = curve.compute_arc_length_reparametrize(500, 5);
945 assert_eq!(pts.len(), 5);
946 for i in 1..pts.len() {
947 let d = dist3(pts[i - 1], pts[i]);
948 assert!(
949 (d - 1.0).abs() < 5e-2,
950 "inter-point distance should be ~1.0, got {d} between pts[{}] and pts[{}]",
951 i - 1,
952 i
953 );
954 }
955 }
956 #[test]
957 fn nurbs_arc_length_table_monotone() {
958 let curve = simple_nurbs_line();
959 let (arc_lengths, _params) = curve.arc_length_table(100);
960 for w in arc_lengths.windows(2) {
961 assert!(
962 w[1] >= w[0] - 1e-12,
963 "arc length table must be non-decreasing"
964 );
965 }
966 }
967}