kernelvex/motion/trajectory.rs
1//! Trajectory representation and sampling utilities.
2//!
3//! This module provides data structures for representing time-parameterized
4//! trajectories and utilities for generating them from curves like Bezier splines.
5//!
6//! # Overview
7//!
8//! A trajectory is a sequence of time-indexed points, each containing:
9//! - **Pose**: Position (x, y) and heading
10//! - **Linear velocity**: Forward speed in m/s
11//! - **Angular velocity**: Rotational speed in rad/s
12//! - **Time**: Timestamp from trajectory start
13//!
14//! Trajectories are used by controllers like RAMSETE and Pure Pursuit to guide
15//! the robot along curved paths.
16//!
17//! # Creating Trajectories
18//!
19//! ## From Bézier curves
20//!
21//! ```no_run
22//! use kernelvex::{Trajectory, Vector2, QTime};
23//!
24//! let trajectory = Trajectory::from_cubic_bezier(
25//! Vector2::new(0.0, 0.0), // Start
26//! Vector2::new(0.5, 0.0), // Control 1
27//! Vector2::new(0.5, 1.0), // Control 2
28//! Vector2::new(1.0, 1.0), // End
29//! QTime::from_sec(3.0), // Total time
30//! 100, // Sample count
31//! 0.5, // Linear velocity
32//! );
33//! ```
34//!
35//! ## Manually
36//!
37//! ```no_run
38//! let mut trajectory = Trajectory::new();
39//! trajectory.push(TrajectoryPoint::new(pose1, 0.5, 0.0, QTime::from_sec(0.0)));
40//! trajectory.push(TrajectoryPoint::new(pose2, 0.5, 0.1, QTime::from_sec(1.0)));
41//! ```
42
43// TODO: add QTime instead of normal f64 type
44use crate::odom::pose::Pose;
45use crate::util::si::{QAngle, QTime, Vector2};
46
47/// A single time-indexed point along a trajectory.
48///
49/// Contains the desired robot state (pose and velocities) at a specific time
50/// during trajectory execution.
51///
52/// # Fields
53///
54/// - `pose`: Desired position and heading
55/// - `linear_velocity`: Desired forward speed (m/s)
56/// - `angular_velocity`: Desired rotation rate (rad/s)
57/// - `time`: Time from trajectory start
58#[derive(Debug, Clone, Copy)]
59pub struct TrajectoryPoint {
60 /// Desired pose (position and heading) at this point.
61 pub pose: Pose,
62 /// Desired linear velocity in meters per second.
63 pub linear_velocity: f64,
64 /// Desired angular velocity in radians per second.
65 pub angular_velocity: f64,
66 /// Time from trajectory start.
67 pub time: QTime,
68}
69
70impl TrajectoryPoint {
71 /// Creates a new trajectory point.
72 ///
73 /// # Arguments
74 ///
75 /// * `pose` - Desired position and heading
76 /// * `linear_velocity` - Forward speed in m/s
77 /// * `angular_velocity` - Rotation rate in rad/s
78 /// * `time` - Time from trajectory start
79 #[inline]
80 pub const fn new(pose: Pose, linear_velocity: f64, angular_velocity: f64, time: QTime) -> Self {
81 Self {
82 pose,
83 linear_velocity,
84 angular_velocity,
85 time,
86 }
87 }
88}
89
90/// A time-parameterized trajectory with sampling support.
91///
92/// Stores a sequence of [`TrajectoryPoint`]s and provides methods for
93/// sampling the trajectory at arbitrary times with interpolation.
94///
95/// # Sampling
96///
97/// Use [`sample`](Self::sample) to get the trajectory state at any time.
98/// The method interpolates between stored points for smooth tracking.
99#[derive(Debug, Clone)]
100pub struct Trajectory {
101 /// Time-ordered trajectory points.
102 points: Vec<TrajectoryPoint>,
103}
104
105impl Trajectory {
106 /// Creates an empty trajectory.
107 #[inline]
108 pub const fn new() -> Self {
109 Self { points: Vec::new() }
110 }
111
112 /// Creates a trajectory from time-ordered points.
113 ///
114 /// # Arguments
115 ///
116 /// * `points` - Vector of trajectory points, must be in ascending time order
117 #[inline]
118 pub const fn from_points(points: Vec<TrajectoryPoint>) -> Self {
119 Self { points }
120 }
121
122 /// Returns a read-only view of trajectory points.
123 #[inline]
124 pub fn points(&self) -> &[TrajectoryPoint] {
125 &self.points
126 }
127
128 /// Returns the total trajectory duration.
129 ///
130 /// # Returns
131 ///
132 /// The time of the last point, or `None` if the trajectory is empty.
133 pub fn total_time(&self) -> Option<QTime> {
134 self.points.last().map(|p| p.time)
135 }
136
137 /// Adds a point to the end of the trajectory.
138 ///
139 /// Points should be added in ascending time order.
140 pub fn push(&mut self, point: TrajectoryPoint) {
141 self.points.push(point);
142 }
143
144 /// Samples the trajectory at the given time with interpolation.
145 ///
146 /// Returns the interpolated trajectory state at the specified time.
147 /// If the time is outside the trajectory bounds, the nearest endpoint
148 /// is returned.
149 ///
150 /// # Arguments
151 ///
152 /// * `time` - Time from trajectory start
153 ///
154 /// # Returns
155 ///
156 /// The interpolated trajectory point, or `None` if the trajectory is empty.
157 ///
158 /// # Interpolation
159 ///
160 /// Position and heading are linearly interpolated. Heading interpolation
161 /// takes the shortest angular path.
162 pub fn sample(&self, time: QTime) -> Option<TrajectoryPoint> {
163 let first = self.points.first()?;
164 let last = self.points.last()?;
165
166 if time.as_sec() <= first.time.as_sec() {
167 return Some(*first);
168 }
169 if time.as_sec() >= last.time.as_sec() {
170 return Some(*last);
171 }
172
173 for window in self.points.windows(2) {
174 let a = window[0];
175 let b = window[1];
176 if time.as_sec() >= a.time.as_sec() && time.as_sec() <= b.time.as_sec() {
177 let span = b.time.as_sec() - a.time.as_sec();
178 let t = if span <= 0.0 {
179 0.0
180 } else {
181 (time.as_sec() - a.time.as_sec()) / span
182 };
183
184 return Some(TrajectoryPoint {
185 pose: interpolate_pose(a.pose, b.pose, t),
186 linear_velocity: lerp(a.linear_velocity, b.linear_velocity, t),
187 angular_velocity: lerp(a.angular_velocity, b.angular_velocity, t),
188 time,
189 });
190 }
191 }
192
193 Some(*last)
194 }
195
196 /// Creates a trajectory by sampling a cubic Bézier curve.
197 ///
198 /// Generates a trajectory by sampling a cubic Bezier spline at regular
199 /// intervals. The heading is derived from the curve tangent, and angular
200 /// velocity is estimated from heading changes between samples.
201 ///
202 /// # Arguments
203 ///
204 /// * `p0` - Start point (meters)
205 /// * `p1` - First control point (meters)
206 /// * `p2` - Second control point (meters)
207 /// * `p3` - End point (meters)
208 /// * `total_time` - Total trajectory duration
209 /// * `samples` - Number of points to generate
210 /// * `linear_velocity` - Constant linear velocity for all points (m/s)
211 ///
212 /// # Returns
213 ///
214 /// A trajectory with `samples` points along the Bézier curve.
215 ///
216 /// # Example
217 ///
218 /// ```no_run
219 /// // S-curve trajectory
220 /// let traj = Trajectory::from_cubic_bezier(
221 /// Vector2::new(0.0, 0.0),
222 /// Vector2::new(1.0, 0.0),
223 /// Vector2::new(0.0, 1.0),
224 /// Vector2::new(1.0, 1.0),
225 /// QTime::from_sec(2.0),
226 /// 50,
227 /// 0.8,
228 /// );
229 /// ```
230 pub fn from_cubic_bezier(
231 p0: Vector2<f64>,
232 p1: Vector2<f64>,
233 p2: Vector2<f64>,
234 p3: Vector2<f64>,
235 total_time: QTime,
236 samples: usize,
237 linear_velocity: f64,
238 ) -> Self {
239 Bezier::new(p0, p1, p2, p3).to_trajectory(total_time, samples, linear_velocity)
240 }
241}
242
243impl Default for Trajectory {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl Default for TrajectoryPoint {
250 fn default() -> Self {
251 Self {
252 pose: Pose::identity(),
253 linear_velocity: 0.,
254 angular_velocity: 0.,
255 time: QTime::default(),
256 }
257 }
258}
259/// Linear interpolation between two values.
260fn lerp(a: f64, b: f64, t: f64) -> f64 {
261 a + (b - a) * t
262}
263
264/// Interpolates between two angles, taking the shortest path.
265fn lerp_angle(a: QAngle, b: QAngle, t: f64) -> QAngle {
266 let delta = (b - a).remainder(QAngle::TAU);
267 a + delta * t
268}
269
270/// Interpolates between two poses.
271fn interpolate_pose(a: Pose, b: Pose, t: f64) -> Pose {
272 let (ax, ay) = (a.position().x, a.position().y);
273 let (bx, by) = (b.position().x, b.position().y);
274 let heading = lerp_angle(a.heading(), b.heading(), t);
275 Pose::new(
276 Vector2::<f64>::new(lerp(ax, bx, t), lerp(ay, by, t)),
277 heading,
278 )
279}
280
281/// A cubic Bézier curve defined by start, end, and two control points.
282///
283/// Bézier curves provide smooth, controllable paths for trajectory generation.
284/// The curve starts at `start`, ends at `end`, and is "pulled" toward the
285/// control points without necessarily passing through them.
286///
287/// # Curve Properties
288///
289/// - Always passes through start and end points
290/// - Tangent at start points toward control1
291/// - Tangent at end points away from control2
292/// - Entire curve lies within convex hull of control points
293///
294/// # Example
295///
296/// ```no_run
297/// let curve = Bezier::new(
298/// Vector2::new(0.0, 0.0), // Start
299/// Vector2::new(1.0, 0.0), // Control 1 - pulls curve right
300/// Vector2::new(0.0, 1.0), // Control 2 - pulls curve up
301/// Vector2::new(1.0, 1.0), // End
302/// );
303///
304/// // Sample point at t=0.5 (middle of curve)
305/// let midpoint = curve.point(0.5);
306/// let heading = curve.heading(0.5);
307/// ```
308#[derive(Debug, Clone, Copy)]
309pub struct Bezier {
310 /// Starting point of the curve.
311 start: Vector2<f64>,
312 /// First control point (influences curve near start).
313 control1: Vector2<f64>,
314 /// Second control point (influences curve near end).
315 control2: Vector2<f64>,
316 /// Ending point of the curve.
317 end: Vector2<f64>,
318}
319
320impl Bezier {
321 /// Maximum parameter value (curves are parameterized from 0 to 1).
322 const T_MAX: f64 = 1.0;
323
324 /// Creates a new cubic Bézier curve.
325 ///
326 /// # Arguments
327 ///
328 /// * `start` - Starting point
329 /// * `control1` - First control point
330 /// * `control2` - Second control point
331 /// * `end` - Ending point
332 #[inline]
333 pub const fn new(
334 start: Vector2<f64>,
335 control1: Vector2<f64>,
336 control2: Vector2<f64>,
337 end: Vector2<f64>,
338 ) -> Self {
339 Self {
340 start,
341 control1,
342 control2,
343 end,
344 }
345 }
346
347 /// Computes the point on the curve at parameter t.
348 ///
349 /// # Arguments
350 ///
351 /// * `t` - Parameter value in range [0, 1]
352 ///
353 /// # Returns
354 ///
355 /// The (x, y) position on the curve at t.
356 ///
357 /// # Panics
358 ///
359 /// Panics if t > 1.0.
360 #[inline]
361 pub fn point(&self, t: f64) -> Vector2<f64> {
362 {
363 assert!(t <= Self::T_MAX, "time cannot exceed 1");
364 }
365
366 let u = 1.0 - t;
367 let tt = t * t;
368 let uu = u * u;
369
370 self.start * (uu * u)
371 + self.control1 * (3.0 * uu * t)
372 + self.control2 * (3.0 * u * tt)
373 + self.end * (tt * t)
374 }
375
376 /// Computes the tangent vector at parameter t.
377 ///
378 /// The tangent points in the direction of motion along the curve.
379 ///
380 /// # Arguments
381 ///
382 /// * `t` - Parameter value in range [0, 1]
383 ///
384 /// # Returns
385 ///
386 /// The tangent vector (not normalized).
387 ///
388 /// # Panics
389 ///
390 /// Panics if t > 1.0.
391 #[inline]
392 pub fn tangent(&self, t: f64) -> Vector2<f64> {
393 {
394 assert!(t <= Self::T_MAX, "time cannot exceed 1");
395 }
396
397 let u = 1.0 - t;
398 let tt = t * t;
399 let uu = u * u;
400
401 (self.control1 - self.start) * (3.0 * uu)
402 + (self.control2 - self.control1) * (6.0 * u * t)
403 + (self.end - self.control2) * (3.0 * tt)
404 }
405
406 /// Computes the heading (direction of motion) at parameter t.
407 ///
408 /// # Arguments
409 ///
410 /// * `t` - Parameter value in range [0, 1]
411 ///
412 /// # Returns
413 ///
414 /// The heading angle derived from the tangent vector.
415 ///
416 /// # Panics
417 ///
418 /// Panics if t > 1.0.
419 #[inline]
420 pub fn heading(&self, t: f64) -> QAngle {
421 {
422 assert!(t <= Self::T_MAX, "time cannot exceed 1");
423 }
424
425 let tan = self.tangent(t);
426 QAngle::from_radians(libm::atan2(tan.y, tan.x))
427 }
428
429 /// Computes the first derivative (velocity vector) at parameter t.
430 ///
431 /// This is equivalent to the tangent vector and represents the rate of
432 /// change of position with respect to the parameter t.
433 ///
434 /// # Arguments
435 ///
436 /// * `t` - Parameter value in range [0, 1]
437 ///
438 /// # Returns
439 ///
440 /// The derivative vector.
441 ///
442 /// # Panics
443 ///
444 /// Panics if t > 1.0.
445 pub fn derivative(&self, t: f64) -> Vector2<f64> {
446 {
447 assert!(t <= Self::T_MAX, "time cannot exceed 1");
448 }
449
450 let u = 1.0 - t;
451
452 let q0 = (self.control1 - self.start) * 3.0;
453 let q1 = (self.control2 - self.control1) * 3.0;
454 let q2 = (self.end - self.control2) * 3.0;
455
456 let velocity = q0 * (u * u) + q1 * (2.0 * u * t) + q2 * (t * t);
457
458 velocity
459 }
460
461 /// Converts the Bézier curve to a trajectory.
462 ///
463 /// Samples the curve at regular intervals and computes angular velocity
464 /// from heading changes between samples.
465 ///
466 /// # Arguments
467 ///
468 /// * `total_time` - Total trajectory duration
469 /// * `samples` - Number of points to generate (must be >= 2)
470 /// * `linear_velocity` - Constant linear velocity for all points (m/s)
471 ///
472 /// # Returns
473 ///
474 /// A trajectory with the specified number of samples, or an empty
475 /// trajectory if `samples < 2`.
476 pub fn to_trajectory(
477 &self,
478 total_time: QTime,
479 samples: usize,
480 linear_velocity: f64,
481 ) -> Trajectory {
482 if samples < 2 {
483 return Trajectory::new();
484 }
485
486 let dt = total_time.as_sec() / (samples as f64 - 1.0);
487 let mut points = Vec::with_capacity(samples);
488 let mut headings = Vec::with_capacity(samples);
489
490 for i in 0..samples {
491 let t = i as f64 / (samples as f64 - 1.0);
492 let pos = self.point(t);
493 let heading = self.heading(t);
494 headings.push(heading);
495 points.push(TrajectoryPoint::new(
496 Pose::new(pos, heading),
497 linear_velocity,
498 0.0,
499 QTime::from_sec(dt * i as f64),
500 ));
501 }
502
503 for i in 0..samples {
504 let angular_velocity = if i + 1 < samples {
505 let dtheta = (headings[i + 1] - headings[i]).remainder(QAngle::TAU);
506 dtheta.as_radians() / dt
507 } else if i > 0 {
508 let dtheta = (headings[i] - headings[i - 1]).remainder(QAngle::TAU);
509 dtheta.as_radians() / dt
510 } else {
511 0.0
512 };
513 points[i].angular_velocity = angular_velocity;
514 }
515
516 Trajectory::from_points(points)
517 }
518}