1#[allow(dead_code)]
4#[derive(Clone)]
5pub struct HapticSample {
6 pub time_ms: f32,
7 pub intensity: f32, pub frequency: f32, pub duration_ms: f32,
10}
11
12#[allow(dead_code)]
13#[derive(Clone, PartialEq, Debug)]
14pub enum HapticActuator {
15 WholeHand,
16 Fingertip,
17 Palm,
18 Wrist,
19 Forearm,
20}
21
22#[allow(dead_code)]
23pub struct HapticTrack {
24 pub actuator: HapticActuator,
25 pub samples: Vec<HapticSample>,
26 pub name: String,
27}
28
29#[allow(dead_code)]
30pub struct HapticExport {
31 pub tracks: Vec<HapticTrack>,
32 pub duration_ms: f32,
33 pub sample_rate_hz: f32,
34 pub device_id: String,
35}
36
37#[allow(dead_code)]
38pub fn new_haptic_export(sample_rate: f32) -> HapticExport {
39 HapticExport {
40 tracks: Vec::new(),
41 duration_ms: 0.0,
42 sample_rate_hz: sample_rate,
43 device_id: String::new(),
44 }
45}
46
47#[allow(dead_code)]
48pub fn add_haptic_track(export: &mut HapticExport, actuator: HapticActuator, name: &str) {
49 export.tracks.push(HapticTrack {
50 actuator,
51 samples: Vec::new(),
52 name: name.to_string(),
53 });
54}
55
56#[allow(dead_code)]
57pub fn add_haptic_sample(
58 export: &mut HapticExport,
59 track_idx: usize,
60 sample: HapticSample,
61) -> bool {
62 if track_idx >= export.tracks.len() {
63 return false;
64 }
65 let end = sample.time_ms + sample.duration_ms;
66 if end > export.duration_ms {
67 export.duration_ms = end;
68 }
69 export.tracks[track_idx].samples.push(sample);
70 true
71}
72
73#[allow(dead_code)]
74pub fn haptic_track_count(export: &HapticExport) -> usize {
75 export.tracks.len()
76}
77
78#[allow(dead_code)]
79pub fn haptic_sample_count(export: &HapticExport, track_idx: usize) -> usize {
80 if track_idx >= export.tracks.len() {
81 return 0;
82 }
83 export.tracks[track_idx].samples.len()
84}
85
86#[allow(dead_code)]
87pub fn export_haptic_json(export: &HapticExport) -> String {
88 let mut out = String::from("{\n");
89 out.push_str(&format!(
90 " \"sample_rate_hz\": {},\n \"duration_ms\": {},\n \"device_id\": \"{}\",\n \"tracks\": [\n",
91 export.sample_rate_hz, export.duration_ms, export.device_id
92 ));
93 for (ti, track) in export.tracks.iter().enumerate() {
94 out.push_str(&format!(
95 " {{ \"name\": \"{}\", \"actuator\": \"{:?}\", \"samples\": [",
96 track.name, track.actuator
97 ));
98 for (si, s) in track.samples.iter().enumerate() {
99 if si > 0 {
100 out.push(',');
101 }
102 out.push_str(&format!(
103 "{{\"time_ms\":{},\"intensity\":{},\"frequency\":{},\"duration_ms\":{}}}",
104 s.time_ms, s.intensity, s.frequency, s.duration_ms
105 ));
106 }
107 out.push(']');
108 out.push('}');
109 if ti + 1 < export.tracks.len() {
110 out.push(',');
111 }
112 out.push('\n');
113 }
114 out.push_str(" ]\n}\n");
115 out
116}
117
118#[allow(dead_code)]
119pub fn export_haptic_csv(export: &HapticExport, track_idx: usize) -> String {
120 if track_idx >= export.tracks.len() {
121 return String::from("time_ms,intensity,frequency,duration_ms\n");
122 }
123 let track = &export.tracks[track_idx];
124 let mut out = String::from("time_ms,intensity,frequency,duration_ms\n");
125 for s in &track.samples {
126 out.push_str(&format!(
127 "{},{},{},{}\n",
128 s.time_ms, s.intensity, s.frequency, s.duration_ms
129 ));
130 }
131 out
132}
133
134#[allow(dead_code)]
135pub fn evaluate_haptic_at(
136 export: &HapticExport,
137 track_idx: usize,
138 time_ms: f32,
139) -> Option<HapticSample> {
140 if track_idx >= export.tracks.len() {
141 return None;
142 }
143 let samples = &export.tracks[track_idx].samples;
144 if samples.is_empty() {
145 return None;
146 }
147 let mut best_idx = 0;
148 let mut best_dist = (samples[0].time_ms - time_ms).abs();
149 for (i, s) in samples.iter().enumerate() {
150 let d = (s.time_ms - time_ms).abs();
151 if d < best_dist {
152 best_dist = d;
153 best_idx = i;
154 }
155 }
156 Some(samples[best_idx].clone())
157}
158
159#[allow(dead_code)]
160pub fn peak_intensity(export: &HapticExport, track_idx: usize) -> f32 {
161 if track_idx >= export.tracks.len() {
162 return 0.0;
163 }
164 export.tracks[track_idx]
165 .samples
166 .iter()
167 .map(|s| s.intensity)
168 .fold(0.0_f32, f32::max)
169}
170
171#[allow(dead_code)]
172pub fn average_intensity(export: &HapticExport, track_idx: usize) -> f32 {
173 if track_idx >= export.tracks.len() {
174 return 0.0;
175 }
176 let samples = &export.tracks[track_idx].samples;
177 if samples.is_empty() {
178 return 0.0;
179 }
180 let sum: f32 = samples.iter().map(|s| s.intensity).sum();
181 sum / samples.len() as f32
182}
183
184#[allow(dead_code)]
185pub fn resample_haptic_track(
186 export: &HapticExport,
187 track_idx: usize,
188 new_rate_hz: f32,
189) -> Vec<HapticSample> {
190 if track_idx >= export.tracks.len() || new_rate_hz <= 0.0 {
191 return Vec::new();
192 }
193 let duration = export.duration_ms;
194 if duration <= 0.0 {
195 return Vec::new();
196 }
197 let step_ms = 1000.0 / new_rate_hz;
198 let count = (duration / step_ms).ceil() as usize;
199 let mut result = Vec::with_capacity(count);
200 for i in 0..count {
201 let t = i as f32 * step_ms;
202 if let Some(s) = evaluate_haptic_at(export, track_idx, t) {
203 result.push(HapticSample {
204 time_ms: t,
205 intensity: s.intensity,
206 frequency: s.frequency,
207 duration_ms: step_ms,
208 });
209 }
210 }
211 result
212}
213
214#[allow(dead_code)]
215pub fn clamp_haptic_intensities(export: &mut HapticExport) {
216 for track in &mut export.tracks {
217 for s in &mut track.samples {
218 s.intensity = s.intensity.clamp(0.0, 1.0);
219 }
220 }
221}
222
223#[allow(dead_code)]
224pub fn haptic_export_duration(export: &HapticExport) -> f32 {
225 export.duration_ms
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn make_sample(time_ms: f32, intensity: f32) -> HapticSample {
233 HapticSample {
234 time_ms,
235 intensity,
236 frequency: 100.0,
237 duration_ms: 10.0,
238 }
239 }
240
241 #[test]
242 fn test_new_export() {
243 let e = new_haptic_export(1000.0);
244 assert_eq!(e.sample_rate_hz, 1000.0);
245 assert_eq!(e.tracks.len(), 0);
246 }
247
248 #[test]
249 fn test_add_track() {
250 let mut e = new_haptic_export(1000.0);
251 add_haptic_track(&mut e, HapticActuator::Palm, "palm");
252 assert_eq!(haptic_track_count(&e), 1);
253 assert_eq!(e.tracks[0].name, "palm");
254 assert_eq!(e.tracks[0].actuator, HapticActuator::Palm);
255 }
256
257 #[test]
258 fn test_add_sample_valid() {
259 let mut e = new_haptic_export(1000.0);
260 add_haptic_track(&mut e, HapticActuator::Wrist, "wrist");
261 let ok = add_haptic_sample(&mut e, 0, make_sample(0.0, 0.5));
262 assert!(ok);
263 }
264
265 #[test]
266 fn test_add_sample_invalid_track() {
267 let mut e = new_haptic_export(1000.0);
268 let ok = add_haptic_sample(&mut e, 99, make_sample(0.0, 0.5));
269 assert!(!ok);
270 }
271
272 #[test]
273 fn test_sample_count() {
274 let mut e = new_haptic_export(1000.0);
275 add_haptic_track(&mut e, HapticActuator::Fingertip, "tip");
276 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.3));
277 add_haptic_sample(&mut e, 0, make_sample(10.0, 0.6));
278 assert_eq!(haptic_sample_count(&e, 0), 2);
279 }
280
281 #[test]
282 fn test_sample_count_invalid() {
283 let e = new_haptic_export(1000.0);
284 assert_eq!(haptic_sample_count(&e, 5), 0);
285 }
286
287 #[test]
288 fn test_json_non_empty() {
289 let mut e = new_haptic_export(500.0);
290 add_haptic_track(&mut e, HapticActuator::WholeHand, "all");
291 add_haptic_sample(&mut e, 0, make_sample(0.0, 1.0));
292 let json = export_haptic_json(&e);
293 assert!(!json.is_empty());
294 assert!(json.contains("sample_rate_hz"));
295 assert!(json.contains("tracks"));
296 }
297
298 #[test]
299 fn test_csv_has_commas() {
300 let mut e = new_haptic_export(1000.0);
301 add_haptic_track(&mut e, HapticActuator::Forearm, "forearm");
302 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.8));
303 let csv = export_haptic_csv(&e, 0);
304 assert!(csv.contains(','));
305 }
306
307 #[test]
308 fn test_peak_intensity() {
309 let mut e = new_haptic_export(1000.0);
310 add_haptic_track(&mut e, HapticActuator::Palm, "p");
311 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.2));
312 add_haptic_sample(&mut e, 0, make_sample(10.0, 0.9));
313 add_haptic_sample(&mut e, 0, make_sample(20.0, 0.5));
314 assert!((peak_intensity(&e, 0) - 0.9).abs() < 1e-5);
315 }
316
317 #[test]
318 fn test_average_intensity() {
319 let mut e = new_haptic_export(1000.0);
320 add_haptic_track(&mut e, HapticActuator::Palm, "p");
321 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.0));
322 add_haptic_sample(&mut e, 0, make_sample(10.0, 1.0));
323 let avg = average_intensity(&e, 0);
324 assert!((avg - 0.5).abs() < 1e-5);
325 }
326
327 #[test]
328 fn test_clamp() {
329 let mut e = new_haptic_export(1000.0);
330 add_haptic_track(&mut e, HapticActuator::Wrist, "w");
331 add_haptic_sample(
332 &mut e,
333 0,
334 HapticSample {
335 time_ms: 0.0,
336 intensity: 1.5,
337 frequency: 100.0,
338 duration_ms: 10.0,
339 },
340 );
341 add_haptic_sample(
342 &mut e,
343 0,
344 HapticSample {
345 time_ms: 10.0,
346 intensity: -0.3,
347 frequency: 100.0,
348 duration_ms: 10.0,
349 },
350 );
351 clamp_haptic_intensities(&mut e);
352 assert!((e.tracks[0].samples[0].intensity - 1.0).abs() < 1e-6);
353 assert!((e.tracks[0].samples[1].intensity - 0.0).abs() < 1e-6);
354 }
355
356 #[test]
357 fn test_evaluate_haptic_at() {
358 let mut e = new_haptic_export(1000.0);
359 add_haptic_track(&mut e, HapticActuator::Palm, "p");
360 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.1));
361 add_haptic_sample(&mut e, 0, make_sample(100.0, 0.9));
362 let s = evaluate_haptic_at(&e, 0, 95.0).expect("should succeed");
363 assert!((s.time_ms - 100.0).abs() < 1e-5);
364 }
365
366 #[test]
367 fn test_duration_updated() {
368 let mut e = new_haptic_export(1000.0);
369 add_haptic_track(&mut e, HapticActuator::Fingertip, "f");
370 add_haptic_sample(
371 &mut e,
372 0,
373 HapticSample {
374 time_ms: 50.0,
375 intensity: 0.5,
376 frequency: 100.0,
377 duration_ms: 30.0,
378 },
379 );
380 assert!((haptic_export_duration(&e) - 80.0).abs() < 1e-5);
381 }
382
383 #[test]
384 fn test_resample() {
385 let mut e = new_haptic_export(1000.0);
386 add_haptic_track(&mut e, HapticActuator::Palm, "p");
387 add_haptic_sample(&mut e, 0, make_sample(0.0, 0.5));
388 add_haptic_sample(&mut e, 0, make_sample(50.0, 0.8));
389 let resampled = resample_haptic_track(&e, 0, 10.0); assert!(!resampled.is_empty());
391 }
392
393 #[test]
394 fn test_track_count_multiple() {
395 let mut e = new_haptic_export(1000.0);
396 add_haptic_track(&mut e, HapticActuator::Palm, "p1");
397 add_haptic_track(&mut e, HapticActuator::Wrist, "w1");
398 add_haptic_track(&mut e, HapticActuator::Forearm, "fa1");
399 assert_eq!(haptic_track_count(&e), 3);
400 }
401
402 #[test]
403 fn test_peak_empty_track() {
404 let e = new_haptic_export(1000.0);
405 assert_eq!(peak_intensity(&e, 0), 0.0);
406 }
407}