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 update(&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 fn denormalize_output(&self, normalized: f32) -> TOut {
140 let min_f = self.config.output_min.as_();
141 let max_f = self.config.output_max.as_();
142
143 let output_f = min_f + normalized * (max_f - min_f);
144 output_f.as_()
145 }
146
147 #[cfg(feature = "grab-mode")]
148 fn apply_grab_mode(&mut self, value: f32) -> f32 {
149 match self.config.grab_mode {
150 GrabMode::None => {
151 self.state.grabbed = true; self.state.virtual_value = value;
154 value
155 }
156
157 GrabMode::Pickup => {
158 if !self.state.grabbed {
159 if value >= self.state.virtual_value {
161 self.state.grabbed = true;
162 } else {
163 return self.state.virtual_value;
165 }
166 }
167 self.state.virtual_value = value;
169 value
170 }
171
172 GrabMode::PassThrough => {
173 if !self.state.grabbed {
174 if !self.state.passthrough_initialized {
176 self.state.last_physical = value;
177 self.state.passthrough_initialized = true;
178 return self.state.virtual_value;
179 }
180
181 let crossing_from_below = value >= self.state.virtual_value
183 && self.state.last_physical < self.state.virtual_value;
184
185 let crossing_from_above = value <= self.state.virtual_value
186 && self.state.last_physical > self.state.virtual_value;
187
188 if crossing_from_below || crossing_from_above {
189 self.state.grabbed = true;
190 self.state.last_physical = value;
191 self.state.virtual_value = value;
192 return value;
193 }
194
195 self.state.last_physical = value;
197 return self.state.virtual_value;
198 }
199 self.state.last_physical = value;
201 self.state.virtual_value = value;
202 value
203 }
204 }
205 }
206
207 #[cfg(feature = "grab-mode")]
213 pub fn physical_position(&self) -> f32 {
214 self.state.physical_position
215 }
216
217 #[cfg(feature = "grab-mode")]
220 pub fn current_output(&self) -> f32 {
221 self.state.virtual_value
222 }
223
224 #[cfg(feature = "grab-mode")]
227 pub fn is_waiting_for_grab(&self) -> bool {
228 matches!(
229 self.config.grab_mode,
230 GrabMode::Pickup | GrabMode::PassThrough
231 ) && !self.state.grabbed
232 }
233
234 #[cfg(feature = "grab-mode")]
237 pub fn set_virtual_value(&mut self, value: f32) {
238 self.state.virtual_value = value;
239 self.state.grabbed = false;
240 self.state.passthrough_initialized = false; }
242
243 #[cfg(feature = "grab-mode")]
250 pub fn release(&mut self) {
251 self.state.virtual_value = self.state.physical_position;
252 self.state.grabbed = false;
253 self.state.passthrough_initialized = false;
254 }
255}