Skip to main content

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}