pamoja_kit/ramp.rs
1//! Easing a value toward a target at a limited rate.
2
3/// Moves a value toward a target by at most a fixed step each update.
4///
5/// Commanding an actuator straight to a new value can be harsh: a motor lurches, a valve
6/// slams, a lamp jumps. A [`Ramp`] limits how fast the commanded value may change, easing it
7/// toward the target by at most `max_step` per update and snapping to the target once it is
8/// within a step. It is the slew-rate limiter behind a smooth start and stop.
9///
10/// # Examples
11///
12/// ```
13/// use pamoja_kit::Ramp;
14///
15/// // Start at 0, move at most 2 per step, aim for 5.
16/// let mut ramp = Ramp::new(0.0, 2.0);
17/// assert_eq!(ramp.update(5.0), 2.0);
18/// assert_eq!(ramp.update(5.0), 4.0);
19/// assert_eq!(ramp.update(5.0), 5.0); // within a step: snaps to the target
20/// ```
21#[derive(Clone, Copy, Debug)]
22pub struct Ramp {
23 value: f32,
24 max_step: f32,
25}
26
27impl Ramp {
28 /// Creates a ramp starting at `start` that moves at most `max_step` per update.
29 ///
30 /// # Arguments
31 ///
32 /// * `start` - the initial value.
33 /// * `max_step` - the largest change allowed per update; its magnitude is used.
34 ///
35 /// # Returns
36 ///
37 /// A ramp resting at `start`.
38 pub fn new(start: f32, max_step: f32) -> Self {
39 Self {
40 value: start,
41 max_step: magnitude(max_step),
42 }
43 }
44
45 /// Moves toward `target` by at most the step and returns the new value.
46 ///
47 /// # Arguments
48 ///
49 /// * `target` - the value being approached.
50 ///
51 /// # Returns
52 ///
53 /// The value after one limited step, equal to `target` once within a step of it.
54 pub fn update(&mut self, target: f32) -> f32 {
55 let step = self.max_step;
56 self.update_capped(target, step)
57 }
58
59 /// Moves toward `target` by at most `max_step` this update, overriding the fixed rate.
60 ///
61 /// This is the variable-rate cousin of [`update`](Ramp::update): a bounded-acceleration
62 /// limiter passes `accel * dt` as the step so the change allowed each update scales with
63 /// the time since the last one, rather than the constant step fixed at construction.
64 ///
65 /// # Arguments
66 ///
67 /// * `target` - the value being approached.
68 /// * `max_step` - the largest change allowed this update; its magnitude is used.
69 ///
70 /// # Returns
71 ///
72 /// The value after one limited step, equal to `target` once within `max_step` of it.
73 pub fn update_capped(&mut self, target: f32, max_step: f32) -> f32 {
74 let step = magnitude(max_step);
75 let delta = target - self.value;
76 if delta > step {
77 self.value += step;
78 } else if delta < -step {
79 self.value -= step;
80 } else {
81 self.value = target;
82 }
83 self.value
84 }
85
86 /// Returns the current value.
87 pub fn value(&self) -> f32 {
88 self.value
89 }
90
91 /// Jumps directly to `value`, bypassing the rate limit.
92 ///
93 /// # Arguments
94 ///
95 /// * `value` - the new value.
96 pub fn set(&mut self, value: f32) {
97 self.value = value;
98 }
99}
100
101// `f32::abs` lives in `std`, so this `no_std` crate takes the magnitude by hand.
102fn magnitude(value: f32) -> f32 {
103 if value < 0.0 {
104 -value
105 } else {
106 value
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn it_climbs_toward_a_higher_target() {
116 let mut ramp = Ramp::new(0.0, 2.0);
117 assert_eq!(ramp.update(5.0), 2.0);
118 assert_eq!(ramp.update(5.0), 4.0);
119 assert_eq!(ramp.update(5.0), 5.0); // snaps within a step
120 assert_eq!(ramp.update(5.0), 5.0); // holds at the target
121 }
122
123 #[test]
124 fn it_falls_toward_a_lower_target() {
125 let mut ramp = Ramp::new(10.0, 3.0);
126 assert_eq!(ramp.update(0.0), 7.0);
127 assert_eq!(ramp.update(0.0), 4.0);
128 assert_eq!(ramp.update(0.0), 1.0);
129 assert_eq!(ramp.update(0.0), 0.0);
130 }
131
132 #[test]
133 fn a_negative_step_is_treated_as_its_magnitude() {
134 let mut ramp = Ramp::new(0.0, -2.0);
135 assert_eq!(ramp.update(10.0), 2.0);
136 }
137
138 #[test]
139 fn set_jumps_past_the_limit() {
140 let mut ramp = Ramp::new(0.0, 1.0);
141 ramp.set(100.0);
142 assert_eq!(ramp.value(), 100.0);
143 }
144
145 #[test]
146 fn update_capped_overrides_the_fixed_rate() {
147 // The construction step is 1.0, but each call here may move up to 0.25.
148 let mut ramp = Ramp::new(0.0, 1.0);
149 assert_eq!(ramp.update_capped(5.0, 0.25), 0.25);
150 assert_eq!(ramp.update_capped(5.0, 0.25), 0.5);
151 // A negative cap is treated as its magnitude.
152 assert_eq!(ramp.update_capped(5.0, -0.5), 1.0);
153 }
154}