1use glam::{Vec2, Vec3};
8
9pub trait Curve: Send + Sync {
13 fn sample(&self, t: f32) -> Vec3;
15
16 fn tangent(&self, t: f32) -> Vec3 {
18 let eps = 1e-4_f32;
19 let t0 = (t - eps).max(0.0);
20 let t1 = (t + eps).min(1.0);
21 (self.sample(t1) - self.sample(t0)) / (t1 - t0)
22 }
23
24 fn unit_tangent(&self, t: f32) -> Vec3 {
26 self.tangent(t).normalize_or_zero()
27 }
28
29 fn normal_2d(&self, t: f32) -> Vec3 {
31 let tan = self.unit_tangent(t);
32 Vec3::new(-tan.y, tan.x, 0.0)
33 }
34
35 fn arc_length(&self, n: u32) -> f32 {
37 let n = n.max(2);
38 let mut len = 0.0_f32;
39 let mut prev = self.sample(0.0);
40 for i in 1..=n {
41 let t = i as f32 / n as f32;
42 let next = self.sample(t);
43 len += (next - prev).length();
44 prev = next;
45 }
46 len
47 }
48
49 fn build_arc_table(&self, n: u32) -> ArcTable {
51 let n = n.max(2) as usize;
52 let mut t_values = Vec::with_capacity(n + 1);
53 let mut arc_lengths = Vec::with_capacity(n + 1);
54 let mut len = 0.0_f32;
55 let mut prev = self.sample(0.0);
56
57 t_values.push(0.0_f32);
58 arc_lengths.push(0.0_f32);
59
60 for i in 1..=n {
61 let t = i as f32 / n as f32;
62 let next = self.sample(t);
63 len += (next - prev).length();
64 t_values.push(t);
65 arc_lengths.push(len);
66 prev = next;
67 }
68
69 let total = len;
71 for v in &mut arc_lengths { *v /= total.max(f32::EPSILON); }
72
73 ArcTable { t_values, arc_lengths, total_length: total }
74 }
75
76 fn sample_uniform(&self, n: usize) -> Vec<Vec3> {
78 (0..n).map(|i| self.sample(i as f32 / (n - 1).max(1) as f32)).collect()
79 }
80
81 fn sample_arc_uniform(&self, n: usize, table_resolution: u32) -> Vec<Vec3> {
83 let table = self.build_arc_table(table_resolution);
84 (0..n).map(|i| {
85 let s = i as f32 / (n - 1).max(1) as f32;
86 let t = table.arc_to_t(s);
87 self.sample(t)
88 }).collect()
89 }
90
91 fn bounding_box(&self, resolution: u32) -> (Vec3, Vec3) {
93 let pts = self.sample_uniform(resolution as usize);
94 let min = pts.iter().copied().fold(Vec3::splat(f32::MAX), |a, p| a.min(p));
95 let max = pts.iter().copied().fold(Vec3::splat(f32::MIN), |a, p| a.max(p));
96 (min, max)
97 }
98
99 fn closest_point(&self, query: Vec3, resolution: u32) -> (f32, Vec3) {
101 let n = resolution.max(2) as usize;
102 let mut best_t = 0.0_f32;
103 let mut best_d2 = f32::MAX;
104 let mut best_pt = self.sample(0.0);
105
106 for i in 0..=n {
107 let t = i as f32 / n as f32;
108 let pt = self.sample(t);
109 let d2 = (pt - query).length_squared();
110 if d2 < best_d2 {
111 best_d2 = d2;
112 best_t = t;
113 best_pt = pt;
114 }
115 }
116 (best_t, best_pt)
117 }
118}
119
120pub struct ArcTable {
124 t_values: Vec<f32>,
125 arc_lengths: Vec<f32>, pub total_length: f32,
127}
128
129impl ArcTable {
130 pub fn arc_to_t(&self, s: f32) -> f32 {
132 let s = s.clamp(0.0, 1.0);
133 let pos = self.arc_lengths.partition_point(|&v| v < s);
135 if pos == 0 { return self.t_values[0]; }
136 if pos >= self.t_values.len() { return *self.t_values.last().unwrap(); }
137 let lo = pos - 1;
138 let hi = pos;
139 let al = self.arc_lengths[lo];
140 let ah = self.arc_lengths[hi];
141 let span = ah - al;
142 if span < f32::EPSILON { return self.t_values[lo]; }
143 let frac = (s - al) / span;
144 self.t_values[lo] + frac * (self.t_values[hi] - self.t_values[lo])
145 }
146}
147
148pub struct LineSegment {
152 pub a: Vec3,
153 pub b: Vec3,
154}
155
156impl LineSegment {
157 pub fn new(a: Vec3, b: Vec3) -> Self { Self { a, b } }
158}
159
160impl Curve for LineSegment {
161 fn sample(&self, t: f32) -> Vec3 {
162 self.a.lerp(self.b, t)
163 }
164 fn tangent(&self, _t: f32) -> Vec3 {
165 self.b - self.a
166 }
167}
168
169pub struct QuadraticBezier {
173 pub p0: Vec3,
174 pub p1: Vec3, pub p2: Vec3,
176}
177
178impl QuadraticBezier {
179 pub fn new(p0: Vec3, p1: Vec3, p2: Vec3) -> Self { Self { p0, p1, p2 } }
180
181 pub fn through_point(p0: Vec3, through: Vec3, p2: Vec3) -> Self {
184 let p1 = through * 2.0 - p0 * 0.5 - p2 * 0.5;
186 Self { p0, p1, p2 }
187 }
188
189 pub fn split(&self, t: f32) -> (Self, Self) {
191 let q0 = self.p0.lerp(self.p1, t);
192 let q1 = self.p1.lerp(self.p2, t);
193 let r0 = q0.lerp(q1, t);
194 (
195 Self::new(self.p0, q0, r0),
196 Self::new(r0, q1, self.p2),
197 )
198 }
199}
200
201impl Curve for QuadraticBezier {
202 fn sample(&self, t: f32) -> Vec3 {
203 let u = 1.0 - t;
204 self.p0 * (u * u) + self.p1 * (2.0 * u * t) + self.p2 * (t * t)
205 }
206 fn tangent(&self, t: f32) -> Vec3 {
207 let u = 1.0 - t;
208 (self.p1 - self.p0) * (2.0 * u) + (self.p2 - self.p1) * (2.0 * t)
209 }
210}
211
212pub struct CubicBezier {
216 pub p0: Vec3,
217 pub p1: Vec3,
218 pub p2: Vec3,
219 pub p3: Vec3,
220}
221
222impl CubicBezier {
223 pub fn new(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> Self {
224 Self { p0, p1, p2, p3 }
225 }
226
227 pub fn ease_in_out(from: Vec3, to: Vec3) -> Self {
229 let dir = (to - from) * 0.33;
230 Self::new(from, from + Vec3::new(dir.x, 0.0, 0.0), to - Vec3::new(dir.x, 0.0, 0.0), to)
231 }
232
233 pub fn split(&self, t: f32) -> (Self, Self) {
235 let q0 = self.p0.lerp(self.p1, t);
236 let q1 = self.p1.lerp(self.p2, t);
237 let q2 = self.p2.lerp(self.p3, t);
238 let r0 = q0.lerp(q1, t);
239 let r1 = q1.lerp(q2, t);
240 let s0 = r0.lerp(r1, t);
241 (
242 Self::new(self.p0, q0, r0, s0),
243 Self::new(s0, r1, q2, self.p3),
244 )
245 }
246
247 pub fn second_derivative(&self, t: f32) -> Vec3 {
249 let d0 = self.p1 - self.p0;
250 let d1 = self.p2 - self.p1;
251 let d2 = self.p3 - self.p2;
252 let e0 = d1 - d0;
253 let e1 = d2 - d1;
254 (e0.lerp(e1, t)) * 6.0
255 }
256
257 pub fn curvature(&self, t: f32) -> f32 {
259 let d1 = self.tangent(t);
260 let d2 = self.second_derivative(t);
261 let cross = d1.cross(d2);
262 cross.length() / d1.length().powi(3)
263 }
264}
265
266impl Curve for CubicBezier {
267 fn sample(&self, t: f32) -> Vec3 {
268 let u = 1.0 - t;
269 let u2 = u * u;
270 let u3 = u2 * u;
271 let t2 = t * t;
272 let t3 = t2 * t;
273 self.p0 * u3 + self.p1 * (3.0 * u2 * t) + self.p2 * (3.0 * u * t2) + self.p3 * t3
274 }
275 fn tangent(&self, t: f32) -> Vec3 {
276 let u = 1.0 - t;
277 let u2 = u * u;
278 let t2 = t * t;
279 (self.p1 - self.p0) * (3.0 * u2)
280 + (self.p2 - self.p1) * (6.0 * u * t)
281 + (self.p3 - self.p2) * (3.0 * t2)
282 }
283}
284
285pub struct BezierN {
289 pub control_points: Vec<Vec3>,
290}
291
292impl BezierN {
293 pub fn new(pts: Vec<Vec3>) -> Self {
294 assert!(pts.len() >= 2, "Need at least 2 control points");
295 Self { control_points: pts }
296 }
297}
298
299impl Curve for BezierN {
300 fn sample(&self, t: f32) -> Vec3 {
301 let mut pts = self.control_points.clone();
303 while pts.len() > 1 {
304 pts = pts.windows(2).map(|w| w[0].lerp(w[1], t)).collect();
305 }
306 pts[0]
307 }
308}
309
310#[derive(Clone, Debug)]
320pub struct CatmullRom {
321 pub points: Vec<Vec3>,
322 pub alpha: f32,
323 pub closed: bool,
324}
325
326impl CatmullRom {
327 pub fn new(points: Vec<Vec3>) -> Self {
328 Self { points, alpha: 0.5, closed: false }
329 }
330
331 pub fn closed(points: Vec<Vec3>) -> Self {
332 Self { points, alpha: 0.5, closed: true }
333 }
334
335 pub fn with_alpha(mut self, alpha: f32) -> Self { self.alpha = alpha; self }
336
337 fn segment_count(&self) -> usize {
338 if self.closed {
339 self.points.len()
340 } else {
341 self.points.len().saturating_sub(1)
342 }
343 }
344
345 fn get_point(&self, i: i32) -> Vec3 {
346 let n = self.points.len() as i32;
347 if self.closed {
348 self.points[((i % n + n) % n) as usize]
349 } else {
350 self.points[i.clamp(0, n - 1) as usize]
351 }
352 }
353
354 fn sample_segment(&self, seg: usize, t: f32) -> Vec3 {
355 let i = seg as i32;
356 let p0 = self.get_point(i - 1);
357 let p1 = self.get_point(i);
358 let p2 = self.get_point(i + 1);
359 let p3 = self.get_point(i + 2);
360
361 let t01 = ((p1 - p0).length() + f32::EPSILON).powf(self.alpha);
363 let t12 = ((p2 - p1).length() + f32::EPSILON).powf(self.alpha);
364 let t23 = ((p3 - p2).length() + f32::EPSILON).powf(self.alpha);
365
366 let t0 = 0.0_f32;
367 let t1 = t0 + t01;
368 let t2 = t1 + t12;
369 let t3 = t2 + t23;
370
371 let tt = t1 + t * t12; let a1 = p0 * ((t1 - tt) / (t1 - t0)) + p1 * ((tt - t0) / (t1 - t0));
374 let a2 = p1 * ((t2 - tt) / (t2 - t1)) + p2 * ((tt - t1) / (t2 - t1));
375 let a3 = p2 * ((t3 - tt) / (t3 - t2)) + p3 * ((tt - t2) / (t3 - t2));
376
377 let b1 = a1 * ((t2 - tt) / (t2 - t0)) + a2 * ((tt - t0) / (t2 - t0));
378 let b2 = a2 * ((t3 - tt) / (t3 - t1)) + a3 * ((tt - t1) / (t3 - t1));
379
380 b1 * ((t2 - tt) / (t2 - t1)) + b2 * ((tt - t1) / (t2 - t1))
381 }
382}
383
384impl Curve for CatmullRom {
385 fn sample(&self, t: f32) -> Vec3 {
386 let n_segs = self.segment_count();
387 if n_segs == 0 { return self.points.first().copied().unwrap_or(Vec3::ZERO); }
388 let t = t.clamp(0.0, 1.0);
389 let scaled = t * n_segs as f32;
390 let seg = (scaled as usize).min(n_segs - 1);
391 let local = scaled - seg as f32;
392 self.sample_segment(seg, local)
393 }
394}
395
396#[derive(Clone, Debug)]
400pub struct HermiteSpline {
401 pub knots: Vec<(Vec3, Vec3)>,
403 pub times: Vec<f32>, }
405
406impl HermiteSpline {
407 pub fn new() -> Self {
408 Self { knots: Vec::new(), times: Vec::new() }
409 }
410
411 pub fn add_knot(mut self, t: f32, pos: Vec3, tangent: Vec3) -> Self {
412 let idx = self.times.partition_point(|&v| v < t);
414 self.times.insert(idx, t);
415 self.knots.insert(idx, (pos, tangent));
416 self
417 }
418
419 fn eval_segment(p0: Vec3, m0: Vec3, p1: Vec3, m1: Vec3, u: f32) -> Vec3 {
421 let u2 = u * u;
422 let u3 = u2 * u;
423 let h00 = 2.0*u3 - 3.0*u2 + 1.0;
424 let h10 = u3 - 2.0*u2 + u;
425 let h01 = -2.0*u3 + 3.0*u2;
426 let h11 = u3 - u2;
427 p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
428 }
429}
430
431impl Default for HermiteSpline {
432 fn default() -> Self { Self::new() }
433}
434
435impl Curve for HermiteSpline {
436 fn sample(&self, t: f32) -> Vec3 {
437 if self.knots.is_empty() { return Vec3::ZERO; }
438 if self.knots.len() == 1 { return self.knots[0].0; }
439
440 let t0 = *self.times.first().unwrap();
441 let t1 = *self.times.last().unwrap();
442 let t = t0 + t.clamp(0.0, 1.0) * (t1 - t0);
443
444 let idx = self.times.partition_point(|&v| v < t).min(self.times.len() - 1);
446 let idx = idx.max(1);
447 let i0 = idx - 1;
448 let i1 = idx;
449
450 let ta = self.times[i0];
451 let tb = self.times[i1];
452 let dt = tb - ta;
453 let u = if dt < f32::EPSILON { 0.0 } else { (t - ta) / dt };
454
455 let (p0, m0) = self.knots[i0];
456 let (p1, m1) = self.knots[i1];
457 Self::eval_segment(p0, m0 * dt, p1, m1 * dt, u)
459 }
460}
461
462pub struct BSpline {
466 pub control_points: Vec<Vec3>,
467 pub closed: bool,
468}
469
470impl BSpline {
471 pub fn new(control_points: Vec<Vec3>) -> Self {
472 Self { control_points, closed: false }
473 }
474
475 pub fn closed(control_points: Vec<Vec3>) -> Self {
476 Self { control_points, closed: true }
477 }
478
479 fn get_point(&self, i: i32) -> Vec3 {
480 let n = self.control_points.len() as i32;
481 if self.closed {
482 self.control_points[((i % n + n) % n) as usize]
483 } else {
484 self.control_points[i.clamp(0, n - 1) as usize]
485 }
486 }
487
488 fn segment_count(&self) -> usize {
489 if self.closed {
490 self.control_points.len()
491 } else {
492 self.control_points.len().saturating_sub(3)
493 }
494 }
495
496 fn sample_segment(&self, seg: usize, u: f32) -> Vec3 {
497 let i = seg as i32;
498 let p0 = self.get_point(i);
499 let p1 = self.get_point(i + 1);
500 let p2 = self.get_point(i + 2);
501 let p3 = self.get_point(i + 3);
502
503 let u2 = u * u;
505 let u3 = u2 * u;
506 let b0 = (1.0 - 3.0*u + 3.0*u2 - u3) / 6.0;
507 let b1 = (4.0 - 6.0*u2 + 3.0*u3) / 6.0;
508 let b2 = (1.0 + 3.0*u + 3.0*u2 - 3.0*u3) / 6.0;
509 let b3 = u3 / 6.0;
510
511 p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3
512 }
513}
514
515impl Curve for BSpline {
516 fn sample(&self, t: f32) -> Vec3 {
517 let n_segs = self.segment_count();
518 if n_segs == 0 { return self.control_points.first().copied().unwrap_or(Vec3::ZERO); }
519 let t = t.clamp(0.0, 1.0);
520 let scaled = t * n_segs as f32;
521 let seg = (scaled as usize).min(n_segs - 1);
522 let local = scaled - seg as f32;
523 self.sample_segment(seg, local)
524 }
525}
526
527pub struct CompositeCurve {
531 segments: Vec<Box<dyn Curve>>,
532 breakpoints: Vec<f32>,
534}
535
536impl CompositeCurve {
537 pub fn new() -> Self {
538 Self { segments: Vec::new(), breakpoints: vec![0.0] }
539 }
540
541 pub fn add_weighted(mut self, seg: Box<dyn Curve>, weight: f32) -> Self {
543 let last = *self.breakpoints.last().unwrap();
544 self.breakpoints.push(last + weight.max(0.0));
545 self.segments.push(seg);
546 self
547 }
548
549 pub fn add(self, seg: Box<dyn Curve>) -> Self {
551 let len = seg.arc_length(64);
552 self.add_weighted(seg, len)
553 }
554
555 fn normalize_breakpoints(&mut self) {
556 let total = *self.breakpoints.last().copied().as_ref().unwrap_or(&1.0);
557 if total > f32::EPSILON {
558 for b in &mut self.breakpoints { *b /= total; }
559 }
560 }
561}
562
563impl Default for CompositeCurve {
564 fn default() -> Self { Self::new() }
565}
566
567impl Curve for CompositeCurve {
568 fn sample(&self, t: f32) -> Vec3 {
569 if self.segments.is_empty() { return Vec3::ZERO; }
570 let t = t.clamp(0.0, 1.0);
571 for i in 0..self.segments.len() {
572 let t0 = self.breakpoints[i];
573 let t1 = self.breakpoints[i + 1];
574 if t <= t1 || i == self.segments.len() - 1 {
575 let span = t1 - t0;
576 let local = if span < f32::EPSILON { 1.0 } else { (t - t0) / span };
577 return self.segments[i].sample(local.clamp(0.0, 1.0));
578 }
579 }
580 self.segments.last().unwrap().sample(1.0)
581 }
582}
583
584pub struct CubicBezier2D {
588 pub p0: Vec2,
589 pub p1: Vec2,
590 pub p2: Vec2,
591 pub p3: Vec2,
592}
593
594impl CubicBezier2D {
595 pub fn new(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) -> Self {
596 Self { p0, p1, p2, p3 }
597 }
598
599 pub fn sample(&self, t: f32) -> Vec2 {
600 let u = 1.0 - t;
601 let u2 = u * u;
602 let u3 = u2 * u;
603 let t2 = t * t;
604 let t3 = t2 * t;
605 self.p0 * u3 + self.p1 * (3.0 * u2 * t) + self.p2 * (3.0 * u * t2) + self.p3 * t3
606 }
607
608 pub fn tangent(&self, t: f32) -> Vec2 {
609 let u = 1.0 - t;
610 let u2 = u * u;
611 let t2 = t * t;
612 (self.p1 - self.p0) * (3.0 * u2)
613 + (self.p2 - self.p1) * (6.0 * u * t)
614 + (self.p3 - self.p2) * (3.0 * t2)
615 }
616
617 pub fn css_ease() -> Self {
620 Self::new(
621 Vec2::ZERO,
622 Vec2::new(0.25, 0.1),
623 Vec2::new(0.25, 1.0),
624 Vec2::ONE,
625 )
626 }
627
628 pub fn css_ease_in() -> Self {
629 Self::new(Vec2::ZERO, Vec2::new(0.42, 0.0), Vec2::ONE, Vec2::ONE)
630 }
631
632 pub fn css_ease_out() -> Self {
633 Self::new(Vec2::ZERO, Vec2::ZERO, Vec2::new(0.58, 1.0), Vec2::ONE)
634 }
635
636 pub fn css_ease_in_out() -> Self {
637 Self::new(Vec2::ZERO, Vec2::new(0.42, 0.0), Vec2::new(0.58, 1.0), Vec2::ONE)
638 }
639
640 pub fn solve_for_x(&self, x: f32, tol: f32) -> f32 {
642 let mut t = x;
643 for _ in 0..8 {
644 let pt = self.sample(t);
645 let err = pt.x - x;
646 if err.abs() < tol { return pt.y; }
647 let dt = self.tangent(t).x;
648 if dt.abs() < 1e-8 { break; }
649 t -= err / dt;
650 t = t.clamp(0.0, 1.0);
651 }
652 self.sample(t).y
653 }
654}
655
656#[derive(Clone, Copy, Debug)]
660pub struct FrenetFrame {
661 pub position: Vec3,
662 pub tangent: Vec3, pub normal: Vec3, pub binormal: Vec3, pub curvature: f32,
666 pub torsion: f32,
667}
668
669impl FrenetFrame {
670 pub fn compute(curve: &dyn Curve, t: f32) -> Self {
672 let eps = 1e-4_f32;
673 let pos = curve.sample(t);
674 let tan = curve.unit_tangent(t);
675
676 let t0 = (t - eps).max(0.0);
678 let t1 = (t + eps).min(1.0);
679 let d0 = curve.unit_tangent(t0);
680 let d1 = curve.unit_tangent(t1);
681 let dT = (d1 - d0) / (t1 - t0);
682
683 let curvature = dT.length();
684 let normal = if curvature > 1e-8 { dT.normalize() } else { tan.any_orthogonal_vector() };
685 let binormal = tan.cross(normal).normalize_or_zero();
686
687 let t00 = (t - 2.0 * eps).max(0.0);
689 let t11 = (t + 2.0 * eps).min(1.0);
690 let bin0 = {
691 let t0 = curve.unit_tangent(t00);
692 let d = (curve.unit_tangent(t00 + eps) - t0) / eps;
693 let n = if d.length() > 1e-8 { d.normalize() } else { Vec3::Y };
694 t0.cross(n)
695 };
696 let bin1 = {
697 let t1 = curve.unit_tangent(t11);
698 let d = (t1 - curve.unit_tangent(t11 - eps)) / eps;
699 let n = if d.length() > 1e-8 { d.normalize() } else { Vec3::Y };
700 t1.cross(n)
701 };
702 let dB = (bin1 - bin0) / (t11 - t00);
703 let torsion = dB.dot(normal);
704
705 Self { position: pos, tangent: tan, normal, binormal, curvature, torsion }
706 }
707}
708
709pub struct CurveWalker<'a> {
713 curve: &'a dyn Curve,
714 table: ArcTable,
715 current: f32, step: f32, }
718
719impl<'a> CurveWalker<'a> {
720 pub fn new(curve: &'a dyn Curve, steps_per_unit_length: f32, resolution: u32) -> Self {
721 let table = curve.build_arc_table(resolution);
722 let step = steps_per_unit_length / table.total_length.max(f32::EPSILON);
723 Self { curve, table, current: 0.0, step }
724 }
725}
726
727impl<'a> Iterator for CurveWalker<'a> {
728 type Item = (f32, Vec3); fn next(&mut self) -> Option<Self::Item> {
731 if self.current > 1.0 { return None; }
732 let s = self.current;
733 let t = self.table.arc_to_t(s);
734 let pos = self.curve.sample(t);
735 self.current += self.step;
736 Some((s, pos))
737 }
738}
739
740#[cfg(test)]
743mod tests {
744 use super::*;
745
746 fn v3(x: f32, y: f32, z: f32) -> Vec3 { Vec3::new(x, y, z) }
747
748 #[test]
749 fn line_segment_endpoints() {
750 let seg = LineSegment::new(v3(0.0, 0.0, 0.0), v3(1.0, 0.0, 0.0));
751 let p0 = seg.sample(0.0);
752 let p1 = seg.sample(1.0);
753 assert!((p0 - v3(0.0, 0.0, 0.0)).length() < 1e-5);
754 assert!((p1 - v3(1.0, 0.0, 0.0)).length() < 1e-5);
755 }
756
757 #[test]
758 fn quadratic_bezier_midpoint() {
759 let q = QuadraticBezier::new(v3(0.0,0.0,0.0), v3(1.0,2.0,0.0), v3(2.0,0.0,0.0));
761 let mid = q.sample(0.5);
762 assert!((mid - v3(1.0, 1.0, 0.0)).length() < 1e-5);
763 }
764
765 #[test]
766 fn cubic_bezier_endpoints() {
767 let b = CubicBezier::new(
768 v3(0.0,0.0,0.0), v3(1.0,2.0,0.0),
769 v3(2.0,-1.0,0.0), v3(3.0,0.0,0.0),
770 );
771 assert!((b.sample(0.0) - v3(0.0,0.0,0.0)).length() < 1e-5);
772 assert!((b.sample(1.0) - v3(3.0,0.0,0.0)).length() < 1e-5);
773 }
774
775 #[test]
776 fn bezier_n_matches_cubic() {
777 let pts = vec![
778 v3(0.0,0.0,0.0), v3(1.0,2.0,0.0),
779 v3(2.0,-1.0,0.0), v3(3.0,0.0,0.0),
780 ];
781 let cubic = CubicBezier::new(pts[0], pts[1], pts[2], pts[3]);
782 let generic = BezierN::new(pts);
783 for i in 0..=10 {
784 let t = i as f32 / 10.0;
785 let d = (cubic.sample(t) - generic.sample(t)).length();
786 assert!(d < 1e-4, "Mismatch at t={}: {}", t, d);
787 }
788 }
789
790 #[test]
791 fn catmull_rom_passes_through_waypoints() {
792 let pts = vec![
793 v3(0.0,0.0,0.0), v3(1.0,1.0,0.0),
794 v3(2.0,0.0,0.0), v3(3.0,1.0,0.0),
795 ];
796 let cr = CatmullRom::new(pts.clone());
797 let start = cr.sample(0.0);
800 let end = cr.sample(1.0);
801 assert!((start - pts[0]).length() < 0.1);
803 assert!((end - pts[3]).length() < 0.1);
804 }
805
806 #[test]
807 fn arc_table_monotone() {
808 let b = CubicBezier::new(
809 v3(0.0,0.0,0.0), v3(0.5,1.0,0.0),
810 v3(1.5,-1.0,0.0), v3(2.0,0.0,0.0),
811 );
812 let table = b.build_arc_table(128);
813 for i in 1..table.arc_lengths.len() {
815 assert!(table.arc_lengths[i] >= table.arc_lengths[i-1]);
816 }
817 }
818
819 #[test]
820 fn arc_to_t_returns_valid_range() {
821 let b = LineSegment::new(v3(0.0,0.0,0.0), v3(1.0,0.0,0.0));
822 let table = b.build_arc_table(64);
823 for i in 0..=10 {
824 let s = i as f32 / 10.0;
825 let t = table.arc_to_t(s);
826 assert!(t >= 0.0 && t <= 1.0, "t={} out of range for s={}", t, s);
827 }
828 }
829
830 #[test]
831 fn bspline_continuity() {
832 let pts = vec![
833 v3(0.0,0.0,0.0), v3(1.0,1.0,0.0), v3(2.0,0.0,0.0),
834 v3(3.0,1.0,0.0), v3(4.0,0.0,0.0),
835 ];
836 let bs = BSpline::new(pts);
837 for i in 0..=20 {
839 let t = i as f32 / 20.0;
840 let p = bs.sample(t);
841 assert!(p.x >= -1.0 && p.x <= 5.0);
842 }
843 }
844
845 #[test]
846 fn hermite_spline_at_knot() {
847 let spl = HermiteSpline::new()
848 .add_knot(0.0, v3(0.0, 0.0, 0.0), v3(1.0, 0.0, 0.0))
849 .add_knot(1.0, v3(2.0, 0.0, 0.0), v3(1.0, 0.0, 0.0));
850 let p0 = spl.sample(0.0);
851 let p1 = spl.sample(1.0);
852 assert!((p0 - v3(0.0, 0.0, 0.0)).length() < 1e-5);
853 assert!((p1 - v3(2.0, 0.0, 0.0)).length() < 1e-5);
854 }
855
856 #[test]
857 fn css_bezier_ease_start_end() {
858 let b = CubicBezier2D::css_ease();
859 let s = b.sample(0.0);
860 let e = b.sample(1.0);
861 assert!((s - Vec2::ZERO).length() < 1e-5);
862 assert!((e - Vec2::ONE ).length() < 1e-5);
863 }
864}