Skip to main content

shuyi_ui/input/
crank.rs

1//! A crank (the Playdate's, ticket 1035) as two controller buttons: each
2//! notch wound backwards is a press and release of [`Button::CrankBack`],
3//! each notch forwards one of [`Button::CrankForward`], so the crank reaches
4//! a game through the player's bindings like any button (game ADR-0034). How
5//! far a notch is comes from the keymap file's `crank` ([`CrankDef`]).
6//!
7//! [`Crank`] is pure: the platform hands it how far the crank turned since
8//! the last frame and gets whole notches back. A notch is that far wound
9//! one way: what is left of one is kept for the next frame, so the same
10//! turn gives the same notches however the frames split it, but turning
11//! the other way starts a fresh notch (so after a notch back, the next
12//! notch forwards is a whole notch away, and a hand resting on the crank
13//! never gives a notch). A docked crank gives none and forgets what was
14//! wound. A backend whose device says how far a crank turned as a float
15//! turns it into whole steps at its boundary, [`STEPS_PER_TURN`] to the turn,
16//! and hands those to [`Crank::turn_steps`]: no float goes further.
17
18use core::cmp::Ordering;
19
20use shuyi_assets::keymap::{Button, ButtonChord, CrankDef};
21
22/// Thousandths of a degree in a degree: a turn in degrees counts in whole
23/// thousandths, so adding them up is exact.
24const MILLI: f32 = 1000.0;
25
26/// How many steps make a whole turn ([`Crank::turn_steps`]).
27pub const STEPS_PER_TURN: i32 = 256;
28
29/// What a crank counts in, a 32,000th of a degree: a thousandth of a degree
30/// and a step (360 / 256 degrees) are each a whole number of them.
31const UNITS_PER_DEGREE: i32 = 32_000;
32
33/// A thousandth of a degree, in those units.
34const UNITS_PER_MILLI: i32 = UNITS_PER_DEGREE / 1000;
35
36/// A step, in those units.
37const UNITS_PER_STEP: i32 = UNITS_PER_DEGREE * 360 / STEPS_PER_TURN;
38
39/// Most steps one frame counts (a hundred turns, as [`MOST_DEGREES`]).
40const MOST_STEPS: i32 = 100 * STEPS_PER_TURN;
41
42/// Most degrees one frame counts (a hundred turns): a turn beyond it is a
43/// broken reading, not a player.
44const MOST_DEGREES: f32 = 36_000.0;
45
46/// Notches wound in one frame: negative backwards, positive forwards.
47pub type Notches = i32;
48
49/// A crank's notches (see the [module docs](self)).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Crank {
52    /// One notch, in units, [`UNITS_PER_DEGREE`] to the degree (at least 1).
53    notch: i32,
54    /// Wound one way since the last notch or the last turn the other
55    /// way, in the same units: always less than a notch.
56    wound: i32,
57}
58
59impl Crank {
60    /// A crank with nothing wound yet, `def.notch_degrees` to the notch.
61    pub fn new(def: CrankDef) -> Self {
62        Self {
63            notch: (i32::from(def.notch_degrees) * UNITS_PER_DEGREE).max(1),
64            wound: 0,
65        }
66    }
67
68    /// The crank turned `degrees` since the last call (forwards is
69    /// positive: clockwise on a Playdate). Returns the notches that turn
70    /// completes; the rest of a notch counts towards the next call in the
71    /// same direction. A reading that isn't a number counts as no turn.
72    pub fn turn(&mut self, degrees: f32) -> Notches {
73        if !degrees.is_finite() {
74            return 0;
75        }
76        let milli = libm::roundf(degrees.clamp(-MOST_DEGREES, MOST_DEGREES) * MILLI);
77        // In range by the clamp above.
78        #[allow(clippy::cast_possible_truncation)]
79        let milli = milli as i32;
80        self.wind(milli * UNITS_PER_MILLI)
81    }
82
83    /// The crank turned `steps` since the last call, [`STEPS_PER_TURN`] to
84    /// the turn, as [`turn`](Self::turn) takes degrees.
85    pub fn turn_steps(&mut self, steps: i32) -> Notches {
86        self.wind(steps.clamp(-MOST_STEPS, MOST_STEPS) * UNITS_PER_STEP)
87    }
88
89    /// Winds `units` more, and gives the notches that completes.
90    fn wind(&mut self, units: i32) -> Notches {
91        // Turned the other way: a fresh notch.
92        if units.signum() == -self.wound.signum() {
93            self.wound = 0;
94        }
95        let wound = self.wound + units;
96        // Towards zero, so the rest keeps the turn's sign and is less than
97        // a notch.
98        let notches = wound / self.notch;
99        self.wound = wound % self.notch;
100        notches
101    }
102
103    /// The crank was docked: whatever was wound towards a notch is
104    /// forgotten.
105    pub fn dock(&mut self) {
106        self.wound = 0;
107    }
108
109    /// The button `notches` press, and how many times: [`Button::CrankBack`]
110    /// for notches backwards, [`Button::CrankForward`] forwards; `None`
111    /// for none.
112    pub fn presses(notches: Notches) -> Option<(Button, u32)> {
113        let button = match notches.cmp(&0) {
114            Ordering::Equal => return None,
115            Ordering::Less => Button::CrankBack,
116            Ordering::Greater => Button::CrankForward,
117        };
118        Some((button, notches.unsigned_abs()))
119    }
120}
121
122/// Whether `chords` are all the buttons of an action and each needs the
123/// crank wound, so that with it docked the action can't be pressed.
124pub fn only_on_the_crank(chords: &[ButtonChord]) -> bool {
125    let wound = |button| matches!(button, Button::CrankBack | Button::CrankForward);
126    !chords.is_empty()
127        && chords.iter().all(|chord| match chord.buttons() {
128            Some((a, b)) => wound(a) || wound(b),
129            None => wound(chord.first()),
130        })
131}
132
133#[cfg(test)]
134mod tests {
135    use proptest::prelude::*;
136
137    use super::*;
138
139    use crate::prelude::*;
140
141    fn crank(notch_degrees: u16) -> Crank {
142        Crank::new(CrankDef { notch_degrees })
143    }
144
145    #[test]
146    fn a_notch_is_forty_five_degrees_by_default() {
147        assert_eq!(CrankDef::default().notch_degrees, 45);
148        let mut c = Crank::new(CrankDef::default());
149        assert_eq!(c.turn(44.0), 0);
150        assert_eq!(c.turn(1.0), 1);
151        assert_eq!(c.turn(-45.0), -1);
152    }
153
154    /// A step is 360 / 256 degrees, whatever the notch.
155    #[test]
156    fn steps_of_a_turn_wind_as_degrees_do() {
157        let mut c = Crank::new(CrankDef::default());
158        assert_eq!(c.turn_steps(31), 0);
159        assert_eq!(c.turn_steps(1), 1, "32 steps are 45 degrees");
160        assert_eq!(c.turn_steps(-32), -1);
161        assert_eq!(c.turn_steps(64), 2, "a quarter turn is two notches");
162        let mut c = crank(30);
163        assert_eq!(c.turn_steps(21), 0, "21 steps are 29.53 degrees");
164        assert_eq!(c.turn_steps(1), 1, "and 22 are 30.94");
165        assert_eq!(c.turn(29.0), 0);
166        assert_eq!(
167            c.turn_steps(1),
168            1,
169            "what was left counts: 0.94, 29 and 1.41"
170        );
171        assert_eq!(c.turn_steps(i32::MAX), 1200, "a hundred turns at most");
172    }
173
174    #[test]
175    fn what_is_left_of_a_notch_carries_over_one_way() {
176        let mut c = crank(30);
177        assert_eq!(c.turn(-20.0), 0);
178        assert_eq!(c.turn(-20.0), -1);
179        // 10 wound backwards: 20 more back is a notch.
180        assert_eq!(c.turn(-20.0), -1);
181        assert_eq!(c.turn(30.0), 1);
182    }
183
184    /// Turning the other way starts a fresh notch: one notch back and a
185    /// little more, then a whole notch forwards is one notch forwards.
186    #[test]
187    fn turning_the_other_way_starts_a_fresh_notch() {
188        let mut c = crank(30);
189        assert_eq!(c.turn(-40.0), -1);
190        assert_eq!(c.turn(29.0), 0);
191        assert_eq!(c.turn(1.0), 1);
192        assert_eq!(c.turn(-20.0), 0);
193        assert_eq!(c.turn(5.0), 0);
194        assert_eq!(c.turn(-29.0), 0);
195        assert_eq!(c.turn(-1.0), -1);
196    }
197
198    #[test]
199    fn a_big_turn_is_several_notches_at_once() {
200        let mut c = crank(30);
201        assert_eq!(c.turn(-95.0), -3);
202        assert_eq!(c.turn(65.0), 2);
203        assert_eq!(c.turn(0.0), 0);
204        assert_eq!(c.turn(25.0), 1, "the 5 left of 65, still forwards");
205    }
206
207    #[test]
208    fn docking_forgets_a_part_notch() {
209        let mut c = crank(30);
210        assert_eq!(c.turn(-25.0), 0);
211        c.dock();
212        assert_eq!(c.turn(-25.0), 0);
213        assert_eq!(c.turn(-5.0), -1);
214    }
215
216    #[test]
217    fn a_broken_reading_is_no_turn() {
218        let mut c = crank(30);
219        assert_eq!(c.turn(f32::NAN), 0);
220        assert_eq!(c.turn(f32::INFINITY), 0);
221        assert_eq!(c.turn(1.0e30), 1200, "a hundred turns at most");
222    }
223
224    #[test]
225    fn an_action_is_only_on_the_crank_when_every_chord_needs_it() {
226        let wound = ButtonChord::single(Button::CrankForward);
227        let back = ButtonChord::single(Button::CrankBack);
228        let plain = ButtonChord::single(Button::East);
229        let chord = |a, b| ButtonChord::pair(a, b).unwrap_or_else(|| panic!());
230        assert!(
231            !only_on_the_crank(&[]),
232            "no button at all isn't the crank's"
233        );
234        assert!(only_on_the_crank(&[wound]));
235        assert!(only_on_the_crank(&[back, wound]));
236        assert!(!only_on_the_crank(&[plain]));
237        assert!(!only_on_the_crank(&[wound, plain]), "a button still works");
238        assert!(only_on_the_crank(&[chord(Button::East, Button::CrankBack)]));
239        assert!(only_on_the_crank(&[chord(Button::CrankBack, Button::East)]));
240        assert!(!only_on_the_crank(&[chord(Button::East, Button::West)]));
241    }
242
243    #[test]
244    fn notches_press_the_crank_buttons() {
245        assert_eq!(Crank::presses(0), None);
246        assert_eq!(Crank::presses(-3), Some((Button::CrankBack, 3)));
247        assert_eq!(Crank::presses(2), Some((Button::CrankForward, 2)));
248    }
249
250    /// The thousandths of a degree in `parts`, each part a frame's turn.
251    fn total(parts: &[i32]) -> i32 {
252        parts.iter().sum()
253    }
254
255    /// The notches each frame gives, one call per part.
256    fn wind(c: &mut Crank, parts: &[i32]) -> Vec<Notches> {
257        #[allow(clippy::cast_precision_loss)] // Small whole numbers.
258        parts.iter().map(|&p| c.turn(p as f32 / MILLI)).collect()
259    }
260
261    proptest! {
262        /// Frame-rate independence: a turn one way split over any number of
263        /// frames gives as many notches as in one frame.
264        #[test]
265        fn any_split_of_a_one_way_turn_gives_the_same_notches(
266            notch in 1u16..=360,
267            parts in prop::collection::vec(0i32..100_000, 0..40),
268            back in any::<bool>(),
269        ) {
270            let parts: Vec<i32> = parts.iter().map(|&p| if back { -p } else { p }).collect();
271            let split: Notches = wind(&mut crank(notch), &parts).iter().sum();
272            let whole = wind(&mut crank(notch), &[total(&parts)]);
273            prop_assert_eq!(split, whole[0]);
274            // A frame never gives notches the other way.
275            for n in wind(&mut crank(notch), &parts) {
276                let one_way = if back { n <= 0 } else { n >= 0 };
277                prop_assert!(one_way, "{} notches", n);
278            }
279        }
280
281        /// A hand wobbling on the crank, never a notch's worth either way
282        /// at a time, never gives a notch.
283        #[test]
284        fn wobbling_by_less_than_a_notch_never_gives_one(
285            notch in 1u16..=360,
286            parts in prop::collection::vec(1i32..1000, 0..40),
287        ) {
288            let notch_milli = i32::from(notch) * 1000;
289            // Back and forth in turns, each less than a notch.
290            let wobble: Vec<i32> = parts
291                .iter()
292                .enumerate()
293                .map(|(i, &p)| {
294                    let p = p * notch_milli / 1000;
295                    let p = p.clamp(1, notch_milli - 1);
296                    if i % 2 == 0 { -p } else { p }
297                })
298                .collect();
299            let notches = wind(&mut crank(notch), &wobble);
300            prop_assert!(notches.iter().all(|&n| n == 0), "{:?}", notches);
301        }
302    }
303}