Skip to main content

repame_input/
haptics.rs

1use std::collections::BTreeMap;
2
3/// A constant-strength motor command. `duration` of zero runs until stopped.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct HapticEffect {
6    pub strong: f32,
7    pub weak: f32,
8    pub duration: f32,
9}
10
11impl HapticEffect {
12    pub fn constant(strong: f32, weak: f32) -> Self {
13        Self {
14            strong,
15            weak,
16            duration: 0.0,
17        }
18    }
19
20    pub fn for_seconds(strong: f32, weak: f32, duration: f32) -> Self {
21        Self {
22            strong,
23            weak,
24            duration: duration.max(0.0),
25        }
26    }
27}
28
29/// Platform hook that receives resolved motor strengths for one device.
30pub trait HapticSink {
31    fn apply(&mut self, device: u64, strong: f32, weak: f32);
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
35struct Motor {
36    strong: f32,
37    weak: f32,
38    remaining: f32,
39    timed: bool,
40}
41
42/// Per-device haptic state with tick-accurate effect expiry.
43#[derive(Debug, Clone, Default, PartialEq)]
44pub struct HapticBus {
45    devices: BTreeMap<u64, Motor>,
46}
47
48impl HapticBus {
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Starts (or replaces) the running effect on a device.
54    pub fn play(&mut self, device: u64, effect: HapticEffect) {
55        self.devices.insert(
56            device,
57            Motor {
58                strong: clamp_motor(effect.strong),
59                weak: clamp_motor(effect.weak),
60                remaining: effect.duration,
61                timed: effect.duration > 0.0,
62            },
63        );
64    }
65
66    pub fn stop(&mut self, device: u64) {
67        self.devices.remove(&device);
68    }
69
70    pub fn stop_all(&mut self) {
71        self.devices.clear();
72    }
73
74    /// Ages every running effect, dropping the ones that ran out.
75    pub fn advance(&mut self, dt_seconds: f32) {
76        if dt_seconds <= 0.0 {
77            return;
78        }
79        for motor in self.devices.values_mut() {
80            if motor.timed {
81                motor.remaining = (motor.remaining - dt_seconds).max(0.0);
82            }
83        }
84        self.devices
85            .retain(|_, motor| !motor.timed || motor.remaining > 0.0);
86    }
87
88    /// Resolved motor strengths in `0..=1`; both zero once the effect expired.
89    pub fn motors(&self, device: u64) -> (f32, f32) {
90        match self.devices.get(&device) {
91            Some(motor) => (motor.strong, motor.weak),
92            None => (0.0, 0.0),
93        }
94    }
95
96    pub fn is_playing(&self, device: u64) -> bool {
97        self.devices.contains_key(&device)
98    }
99
100    pub fn active_devices(&self) -> impl Iterator<Item = u64> + '_ {
101        self.devices.keys().copied()
102    }
103
104    /// Feeds every active device to a platform sink.
105    pub fn apply_to(&self, sink: &mut dyn HapticSink) {
106        for (&device, motor) in &self.devices {
107            sink.apply(device, motor.strong, motor.weak);
108        }
109    }
110}
111
112fn clamp_motor(value: f32) -> f32 {
113    if value.is_nan() {
114        return 0.0;
115    }
116    value.clamp(0.0, 1.0)
117}