1use num_traits::AsPrimitive;
2
3use crate::config::{Config, ConfigError};
4use crate::filters::NoiseFilter;
5use crate::state::State;
6
7#[cfg(feature = "grab-mode")]
8use crate::grab_mode::GrabMode;
9
10use crate::filters::EmaFilter;
11
12#[cfg(feature = "moving-average")]
13use crate::filters::MovingAvgFilter;
14
15pub struct PotHead<TIn: 'static, TOut: 'static = TIn> {
16 config: &'static Config<TIn, TOut>,
17 state: State<f32>,
18}
19
20impl<TIn, TOut> PotHead<TIn, TOut>
21where
22 TIn: 'static + Copy + PartialOrd + AsPrimitive<f32>,
23 TOut: 'static + Copy + PartialOrd + AsPrimitive<f32>,
24 f32: AsPrimitive<TOut>,
25{
26 pub fn new(config: &'static Config<TIn, TOut>) -> Result<Self, ConfigError> {
27 config.validate()?;
28
29 let mut state = State::default();
30
31 if matches!(config.filter, NoiseFilter::ExponentialMovingAverage { .. }) {
33 state.ema_filter = Some(EmaFilter::new());
34 }
35
36 #[cfg(feature = "moving-average")]
37 if let NoiseFilter::MovingAverage { window_size } = config.filter {
38 state.ma_filter = Some(MovingAvgFilter::new(window_size));
39 }
40
41 Ok(Self { config, state })
42 }
43
44 pub fn config(&self) -> &Config<TIn, TOut> {
45 self.config
46 }
47
48 pub fn process(&mut self, input: TIn) -> TOut {
49 let normalized = self.normalize_input(input);
51
52 let filtered = self.apply_filter(normalized);
54
55 let curved = self.config.curve.apply(filtered);
57
58 let hysteresis_applied = self
60 .config
61 .hysteresis
62 .apply(curved, &mut self.state.hysteresis);
63
64 #[cfg(feature = "grab-mode")]
66 {
67 self.state.physical_position = hysteresis_applied;
68 }
69
70 let snapped = self.apply_snap_zones(hysteresis_applied);
72
73 #[cfg(feature = "grab-mode")]
75 let output = self.apply_grab_mode(snapped);
76
77 #[cfg(not(feature = "grab-mode"))]
78 let output = snapped;
79
80 self.state.last_output = output;
82
83 self.denormalize_output(output)
85 }
86
87 fn apply_filter(&mut self, value: f32) -> f32 {
88 match &self.config.filter {
89 NoiseFilter::None => value,
90
91 NoiseFilter::ExponentialMovingAverage { alpha } => {
92 if let Some(ref mut filter) = self.state.ema_filter {
93 filter.apply(value, *alpha)
94 } else {
95 value
96 }
97 }
98
99 #[cfg(feature = "moving-average")]
100 NoiseFilter::MovingAverage { .. } => {
101 if let Some(ref mut filter) = self.state.ma_filter {
102 filter.apply(value)
103 } else {
104 value
105 }
106 }
107 }
108 }
109
110 fn apply_snap_zones(&self, value: f32) -> f32 {
111 for zone in self.config.snap_zones {
113 if zone.contains(value) {
114 return zone.apply(value, self.state.last_output);
115 }
116 }
117 value }
119
120 fn normalize_input(&self, input: TIn) -> f32 {
121 let input_f = input.as_();
122 let min_f = self.config.input_min.as_();
123 let max_f = self.config.input_max.as_();
124
125 let clamped = if input_f < min_f {
127 min_f
128 } else if input_f > max_f {
129 max_f
130 } else {
131 input_f
132 };
133
134 (clamped - min_f) / (max_f - min_f)
137 }
138
139 #[cfg(feature = "grab-mode")]
140 fn normalize_output(&self, value: TOut) -> f32 {
141 let value_f = value.as_();
142 let min_f = self.config.output_min.as_();
143 let max_f = self.config.output_max.as_();
144 (value_f - min_f) / (max_f - min_f)
145 }
146
147 fn denormalize_output(&self, normalized: f32) -> TOut {
148 let min_f = self.config.output_min.as_();
149 let max_f = self.config.output_max.as_();
150
151 let output_f = min_f + normalized * (max_f - min_f);
152 output_f.as_()
153 }
154
155 #[cfg(feature = "grab-mode")]
156 fn apply_grab_mode(&mut self, value: f32) -> f32 {
157 match self.config.grab_mode {
158 GrabMode::None => {
159 self.state.grabbed = true; self.state.virtual_value = value;
162 value
163 }
164
165 GrabMode::Pickup => {
166 if !self.state.grabbed {
167 if value >= self.state.virtual_value {
169 self.state.grabbed = true;
170 } else {
171 return self.state.virtual_value;
173 }
174 }
175 self.state.virtual_value = value;
177 value
178 }
179
180 GrabMode::PassThrough => {
181 if !self.state.grabbed {
182 if !self.state.passthrough_initialized {
184 self.state.last_physical = value;
185 self.state.passthrough_initialized = true;
186 return self.state.virtual_value;
187 }
188
189 let crossing_from_below = value >= self.state.virtual_value
191 && self.state.last_physical < self.state.virtual_value;
192
193 let crossing_from_above = value <= self.state.virtual_value
194 && self.state.last_physical > self.state.virtual_value;
195
196 if crossing_from_below || crossing_from_above {
197 self.state.grabbed = true;
198 self.state.last_physical = value;
199 self.state.virtual_value = value;
200 return value;
201 }
202
203 self.state.last_physical = value;
205 return self.state.virtual_value;
206 }
207 self.state.last_physical = value;
209 self.state.virtual_value = value;
210 value
211 }
212 }
213 }
214
215 #[cfg(feature = "grab-mode")]
221 pub fn physical_position(&self) -> f32 {
222 self.state.physical_position
223 }
224
225 #[cfg(feature = "grab-mode")]
228 pub fn current_output(&self) -> f32 {
229 self.state.virtual_value
230 }
231
232 #[cfg(feature = "grab-mode")]
235 pub fn is_waiting_for_grab(&self) -> bool {
236 matches!(
237 self.config.grab_mode,
238 GrabMode::Pickup | GrabMode::PassThrough
239 ) && !self.state.grabbed
240 }
241
242 #[cfg(feature = "grab-mode")]
250 pub fn set_virtual_value(&mut self, value: TOut) {
251 self.state.virtual_value = self.normalize_output(value);
252 self.state.grabbed = false;
253 self.state.passthrough_initialized = false;
254 }
255
256 #[cfg(feature = "grab-mode")]
267 pub fn attach(&mut self, current_input: TIn) {
268 let normalized = self.normalize_input(current_input);
269 if let Some(ref mut ema) = self.state.ema_filter {
270 ema.reset(normalized);
271 }
272 self.state.grabbed = false;
273 self.state.passthrough_initialized = false;
274 }
275
276 #[cfg(feature = "grab-mode")]
288 pub fn detach(&mut self) {
289 if self.state.grabbed {
290 self.state.virtual_value = self.state.last_output;
291 }
292 self.state.grabbed = false;
293 self.state.passthrough_initialized = false;
294 }
295}