Skip to main content

oxihuman_export/
haptic_export.rs

1//! Haptic feedback force profile export.
2
3#[allow(dead_code)]
4#[derive(Clone)]
5pub struct HapticSample {
6    pub time_ms: f32,
7    pub intensity: f32, // 0..1
8    pub frequency: f32, // Hz
9    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); // 10 Hz
390        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}