1use crate::manifest::TrailPoint;
2
3pub fn filter_trail(points: &[TrailPoint], threshold: f32) -> Vec<TrailPoint> {
7 let threshold_sq = threshold * threshold;
8 let mut kept: Vec<TrailPoint> = Vec::with_capacity(points.len());
9
10 for point in points {
11 let keep = match kept.last() {
12 None => true,
13 Some(last) => {
14 let dx = point.x - last.x;
15 let dy = point.y - last.y;
16 dx * dx + dy * dy > threshold_sq
17 }
18 };
19 if keep {
20 kept.push(point.clone());
21 }
22 }
23
24 kept
25}
26
27pub fn trail_opacity(point_time_ms: u64, current_time_ms: u64, fade_duration_ms: u64) -> f32 {
31 if current_time_ms < point_time_ms || fade_duration_ms == 0 {
32 return 0.0;
33 }
34 let elapsed = current_time_ms - point_time_ms;
35 if elapsed >= fade_duration_ms {
36 return 0.0;
37 }
38 1.0 - (elapsed as f32 / fade_duration_ms as f32)
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 fn pt(time_ms: u64, x: f32, y: f32) -> TrailPoint {
46 TrailPoint { time_ms, x, y }
47 }
48
49 #[test]
50 fn filter_removes_close_points() {
51 let points = vec![pt(0, 0.0, 0.0), pt(10, 1.0, 1.0), pt(20, 100.0, 100.0)];
52 let result = filter_trail(&points, 5.0);
54 assert_eq!(result.len(), 2);
55 assert_eq!(result[0].x, 0.0);
56 assert_eq!(result[1].x, 100.0);
57 }
58
59 #[test]
60 fn filter_keeps_all_if_threshold_zero() {
61 let points = vec![pt(0, 0.0, 0.0), pt(10, 0.1, 0.1)];
62 assert_eq!(filter_trail(&points, 0.0).len(), 2);
63 }
64
65 #[test]
66 fn opacity_full_at_trigger_time() {
67 assert!((trail_opacity(1000, 1000, 500) - 1.0).abs() < f32::EPSILON);
68 }
69
70 #[test]
71 fn opacity_zero_before_trigger() {
72 assert_eq!(trail_opacity(1000, 500, 500), 0.0);
73 }
74
75 #[test]
76 fn opacity_zero_after_fade() {
77 assert_eq!(trail_opacity(1000, 1600, 500), 0.0);
78 }
79
80 #[test]
81 fn opacity_half_at_midpoint() {
82 let op = trail_opacity(1000, 1250, 500);
83 assert!((op - 0.5).abs() < 1e-5);
84 }
85}