1use std::f64::consts::PI;
7
8use glam::{DVec2, DVec3};
9use lerp::Lerp;
10use thiserror::Error;
11
12use super::FrenetFrame;
13use itertools::Itertools;
14
15pub type PathResult<T> = Result<T, PathError>;
16
17pub trait Path {
19 fn point(&self, t: f64) -> DVec3;
21
22 fn frame(&self, t: f64) -> FrenetFrame;
29}
30
31impl Path for Box<dyn Path + '_> {
33 fn point(&self, t: f64) -> DVec3 {
34 (**self).point(t)
35 }
36 fn frame(&self, t: f64) -> FrenetFrame {
37 (**self).frame(t)
38 }
39}
40
41#[derive(Error, Debug)]
42pub enum PathError {
43 #[error("{0}")]
44 SinusoidError(#[from] SinusoidError),
45 #[error("{0}")]
46 BezierError(#[from] BezierError),
47 #[error("{0}")]
48 CatenaryError(#[from] CatenaryError),
49 #[error("{0}")]
50 SerpentineError(#[from] SerpentineError),
51}
52
53#[derive(Debug, Clone)]
60pub struct Line {
61 x: f64,
62 y: f64,
63 z: f64,
64}
65
66impl Line {
67 pub fn new(x: f64, y: f64, z: f64) -> Self {
68 Self { x, y, z }
69 }
70}
71
72impl Path for Line {
73 fn point(&self, t: f64) -> DVec3 {
74 DVec3 {
75 x: self.x,
76 y: self.y,
77 z: self.z,
78 } * t
79 }
80 fn frame(&self, _t: f64) -> FrenetFrame {
81 let tangent = DVec3 {
82 x: self.x,
83 y: self.y,
84 z: self.z,
85 }
86 .normalize_or_zero();
87 let normal = DVec3 {
88 x: -self.y,
89 y: self.x,
90 z: 0.0,
91 }
92 .normalize_or_zero();
93 let binormal = tangent.cross(normal);
94 FrenetFrame {
95 tangent,
96 normal,
97 binormal,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
106pub struct Revolve {
107 radius: f64,
108 start_angle_rad: f64,
109 end_angle_rad: f64,
110}
111
112impl Revolve {
113 pub fn new(radius: f64, start_angle: f64, end_angle: f64) -> Self {
114 Self {
115 radius,
116 start_angle_rad: start_angle * PI / 180.0,
117 end_angle_rad: end_angle * PI / 180.0,
118 }
119 }
120}
121
122impl Path for Revolve {
123 fn point(&self, t: f64) -> DVec3 {
124 let theta = self.start_angle_rad.lerp(self.end_angle_rad, t);
125 DVec3 {
126 x: self.radius * theta.cos(),
127 y: self.radius * theta.sin(),
128 z: 0.0,
129 }
130 }
131 fn frame(&self, t: f64) -> FrenetFrame {
132 let theta = self.start_angle_rad.lerp(self.end_angle_rad, t);
133 FrenetFrame {
134 tangent: DVec3 {
135 x: -theta.sin(),
136 y: theta.cos(),
137 z: 0.0,
138 },
139 normal: DVec3 {
140 x: -theta.cos(),
141 y: -theta.sin(),
142 z: 0.0,
143 },
144 binormal: DVec3 {
145 x: 0.0,
146 y: 0.0,
147 z: 1.0,
148 },
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
160pub struct Sinusoid {
161 amplitude: f64,
162 period: f64,
163 phase: f64,
164 start: f64,
165 end: f64,
166}
167
168impl Sinusoid {
169 pub fn new(amplitude: f64, period: f64, phase: f64, start: f64, end: f64) -> PathResult<Self> {
170 if period <= 0.0 {
171 return Err(SinusoidError::SinusoidInfiniteFrequency(period))?;
172 }
173 Ok(Self {
174 amplitude,
175 period,
176 phase,
177 start,
178 end,
179 })
180 }
181}
182
183impl Path for Sinusoid {
184 fn point(&self, t: f64) -> DVec3 {
185 let x = self.start.lerp(self.end, t);
186 let omega = 2.0 * PI / self.period;
187 DVec3 {
188 x,
189 y: 0.0,
190 z: self.amplitude * f64::sin(omega * (x + self.phase)),
191 }
192 }
193 fn frame(&self, t: f64) -> FrenetFrame {
194 let x = self.start.lerp(self.end, t);
195 let omega = 2.0 * PI / self.period;
196 FrenetFrame {
197 tangent: DVec3 {
198 x: 1.0,
199 y: 0.0,
200 z: self.amplitude * f64::cos(omega * (x + self.phase)) * omega,
201 }
202 .normalize_or_zero(),
203 normal: DVec3 {
204 x: 0.0,
205 y: 1.0,
206 z: 0.0,
207 },
208 binormal: DVec3 {
209 x: -self.amplitude * f64::cos(omega * (x + self.phase)) * omega,
210 y: 0.0,
211 z: 1.0,
212 }
213 .normalize_or_zero(),
214 }
215 }
216}
217
218#[derive(Error, Debug)]
219pub enum SinusoidError {
220 #[error("Period of {0} is invalid; must be positive")]
221 SinusoidInfiniteFrequency(f64),
222}
223
224#[derive(Debug, Clone)]
229pub struct Bezier {
230 points: Vec<DVec2>,
231}
232
233impl Bezier {
234 pub fn new(points: Vec<DVec2>) -> Result<Self, BezierError> {
235 if points.len() < 2 {
236 return Err(BezierError::NotEnoughPoints(points.len()));
237 }
238 Ok(Self { points })
239 }
240}
241
242impl Path for Bezier {
243 fn point(&self, t: f64) -> DVec3 {
244 let point2d = bezier(&self.points, t);
245 DVec3 {
246 x: point2d.x,
247 y: 0.0,
248 z: point2d.y,
249 }
250 }
251 fn frame(&self, t: f64) -> FrenetFrame {
252 let point2d = bezier_derivative(&self.points, t);
253 let tangent = DVec3 {
254 x: point2d.x,
255 y: 0.0,
256 z: point2d.y,
257 }
258 .normalize_or_zero();
259 let normal = DVec3 {
260 x: 0.0,
261 y: 1.0,
262 z: 0.0,
263 };
264 let binormal = tangent.cross(normal);
265 FrenetFrame {
266 tangent,
267 normal,
268 binormal,
269 }
270 }
271}
272
273#[derive(Error, Debug)]
274pub enum BezierError {
275 #[error("Bezier curve requires at least two points; {0} provided")]
276 NotEnoughPoints(usize),
277}
278
279fn bezier(points: &Vec<DVec2>, t: f64) -> DVec2 {
280 let result = recursive_bezier(points, t);
281 result[0]
282}
283
284#[allow(clippy::ptr_arg)]
286fn recursive_bezier(points: &Vec<DVec2>, t: f64) -> Vec<DVec2> {
287 if points.len() == 1 {
288 vec![points[0]]
289 } else {
290 recursive_bezier(
291 &points
292 .iter()
293 .tuple_windows()
294 .map(|(point1, point2)| point1.lerp(*point2, t))
295 .collect(),
296 t,
297 )
298 }
299}
300
301fn bezier_derivative(points: &[DVec2], t: f64) -> DVec2 {
302 let n = points.len() as f64;
303 let intersparsed_points = points
304 .iter()
305 .tuple_windows()
306 .map(|(point1, point2)| n * (point2 - point1))
307 .collect();
308 bezier(&intersparsed_points, t)
309}
310
311#[derive(Debug, Clone)]
316pub struct Catenary {
317 a: f64,
318 k: f64,
319 c: f64,
320 span: f64,
321}
322
323impl Catenary {
324 pub fn new(span: f64, height: f64, s: f64) -> Result<Self, CatenaryError> {
325 let v = height;
327 let h = span;
328
329 let initial_guess = 1.0
330 / f64::sqrt(f64::sqrt(f64::powi(s, 2) - f64::powi(v, 2)) / h - 1.0)
331 / (2.0 * f64::sqrt(6.0));
332
333 let min_s = f64::sqrt(f64::powi(height, 2) + f64::powi(span, 2));
334 if s <= min_s {
335 return Err(CatenaryError::LengthTooShort {
336 given: s,
337 min: min_s,
338 })?;
339 }
340
341 let a = newton_a(v, h, s, 0.0, 0.0, span, height, initial_guess)?;
342
343 let k: f64 = 0.5 * (h - a * f64::ln((s + v) / (s - v)));
345 let c: f64 = -a * f64::cosh((-k) / a);
346
347 Ok(Self { a, k, c, span })
348 }
349}
350
351impl Path for Catenary {
352 fn point(&self, t: f64) -> DVec3 {
353 let x = 0.0.lerp(self.span, t);
354 DVec3 {
355 x,
356 y: 0.0,
357 z: catenary(x, self.a, self.k, self.c),
358 }
359 }
360 fn frame(&self, t: f64) -> FrenetFrame {
361 let x = 0.0.lerp(self.span, t);
362 let tangent = DVec3 {
363 x: 1.0,
364 y: 0.0,
365 z: f64::sinh((x - self.k) / self.a), }
367 .normalize_or_zero();
368 let normal = DVec3 {
369 x: 0.0,
370 y: 1.0,
371 z: 0.0,
372 };
373 let binormal = tangent.cross(normal);
374 FrenetFrame {
375 tangent,
376 normal,
377 binormal,
378 }
379 }
380}
381
382fn catenary(x: f64, a: f64, k: f64, c: f64) -> f64 {
383 a * f64::cosh((x - k) / a) + c
384}
385
386#[allow(clippy::too_many_arguments)]
388fn newton_a(
389 v: f64,
390 h: f64,
391 s: f64,
392 x0: f64,
393 z0: f64,
394 x1: f64,
395 z1: f64,
396 initial_guess: f64,
397) -> Result<f64, CatenaryError> {
398 let iteration_limit = 10_000;
399
400 let epsilon: f64 = f64::powi(10.0, -9);
404
405 let mut b_i: f64 = initial_guess;
407 let mut b_ip1: f64 = b_i;
408 let mut icount: i32 = 0;
409 while catenary_bounds_err(x0, z0, x1, z1, b_ip1 * h, s) > epsilon && icount < iteration_limit {
410 b_i = b_ip1;
411 b_ip1 = b_i - f2b(b_i, v, h, s) / df2b(b_i);
412 icount += 1;
413 }
414 if icount >= iteration_limit
416 || !f64::is_finite(catenary_bounds_err(x0, z0, x1, z1, b_ip1 * h, s))
417 {
418 Err(CatenaryError::NewtonFail {
419 iterations: iteration_limit,
420 initial: initial_guess,
421 })
422 } else {
423 Ok(b_ip1 * h)
424 }
425}
426
427fn f2b(b: f64, v: f64, h: f64, s: f64) -> f64 {
428 1.0 / f64::sqrt(2.0 * b * f64::sinh(1.0 / (2.0 * b)) - 1.0)
429 - 1.0 / f64::sqrt(f64::sqrt(f64::powi(s, 2) - f64::powi(v, 2)) / h - 1.0)
430}
431
432fn df2b(b: f64) -> f64 {
433 let m = 1.0 / (2.0 * b);
434 (m * f64::cosh(m) - f64::sinh(m)) * f64::powf(1.0 / m * f64::sinh(m) - 1.0, -1.5)
435}
436
437fn catenary_bounds_err(x0: f64, z0: f64, x1: f64, z1: f64, a: f64, s: f64) -> f64 {
438 let v = z1 - z0;
439 let h = x1 - x0;
440 let k: f64 = x0 + 0.5 * (h - a * f64::ln((s + v) / (s - v)));
441 let c: f64 = z0 - a * f64::cosh((x0 - k) / a);
442 let error0 = f64::abs(catenary(x0, a, k, c) - z0);
443 let error1 = f64::abs(catenary(x1, a, k, c) - z1);
444 f64::max(error0, error1)
445}
446
447#[derive(Error, Debug)]
448pub enum CatenaryError {
449 #[error("Given length {given} is too short; must be greater than {min}.")]
450 LengthTooShort { given: f64, min: f64 },
451 #[error(
452 "Newton's method failed to converge to an accurate solution after {iterations} iterations. The initial guess was {initial}. Change the parameters to a less extreme catenary curve, or try again with a different initial guess."
453 )]
454 NewtonFail { iterations: i32, initial: f64 },
455}
456
457#[derive(Debug, Clone)]
463pub struct Serpentine {
464 x: f64,
465 z: f64,
466}
467
468impl Serpentine {
469 pub fn new(x: f64, z: f64) -> Result<Self, SerpentineError> {
470 if z <= 0.0 {
471 return Err(SerpentineError::OrderedHeight);
472 }
473 if z > x {
474 return Err(SerpentineError::TooTall);
475 }
476
477 Ok(Self { x, z })
478 }
479}
480
481impl Path for Serpentine {
482 fn point(&self, t: f64) -> DVec3 {
484 let xm = self.x / 2.0;
485 let zm = self.z / 2.0;
486
487 let zd = self.z - zm;
488 let xd = self.x - xm;
489
490 let r0 = ((zm * zm) + (xm * xm)) / (2.0 * zm);
491 let r1 = ((zd * zd) + (xd * xd)) / (2.0 * zd);
492
493 let theta0_start = -PI / 2.0;
494 let theta0_end = f64::asin(xm / r0) - PI / 2.0;
495
496 let theta1_start = PI / 2.0 + f64::asin(xd / r1);
497 let theta1_end = PI / 2.0;
498
499 if t < 0.5 {
500 let theta = theta0_start.lerp(theta0_end, t * 2.0);
501 DVec3 {
502 x: r0 * f64::cos(theta),
503 y: 0.0,
504 z: r0 * f64::sin(theta) + r0,
505 }
506 } else {
507 let theta = theta1_start.lerp(theta1_end, (t - 0.5) * 2.0);
508 DVec3 {
509 x: r1 * f64::cos(theta) + self.x,
510 y: 0.0,
511 z: r1 * f64::sin(theta) - r0 + self.z,
512 }
513 }
514 }
515 fn frame(&self, t: f64) -> FrenetFrame {
516 let xm = self.x / 2.0;
517 let zm = self.z / 2.0;
518
519 let zd = self.z - zm;
520 let xd = self.x - xm;
521
522 let r0 = ((zm * zm) + (xm * xm)) / (2.0 * zm);
523 let r1 = ((zd * zd) + (xd * xd)) / (2.0 * zd);
524
525 let theta0_start = -PI / 2.0;
526 let theta0_end = f64::asin(xm / r0) - PI / 2.0;
527
528 let theta1_start = PI / 2.0 + f64::asin(xd / r1);
529 let theta1_end = PI / 2.0;
530
531 let tangent = if t < 0.5 {
532 let theta = theta0_start.lerp(theta0_end, t * 2.0);
533 DVec3 {
534 x: -f64::sin(theta),
535 y: 0.0,
536 z: f64::cos(theta),
537 }
538 } else {
539 let theta = theta1_start.lerp(theta1_end, (t - 0.5) * 2.0);
540 DVec3 {
541 x: f64::sin(theta),
542 y: 0.0,
543 z: -f64::cos(theta),
544 }
545 };
546 let normal = DVec3 {
547 x: 0.0,
548 y: 1.0,
549 z: 0.0,
550 };
551 let binormal = tangent.cross(normal);
552 FrenetFrame {
553 tangent,
554 normal,
555 binormal,
556 }
557 }
558}
559
560#[derive(Error, Debug)]
561pub enum SerpentineError {
562 #[error("Ending height must be greater than the starting height.")]
563 OrderedHeight,
564 #[error("Serpentine curve height cannot be greater than its length.")]
565 TooTall,
566}