1use egui::Pos2;
12
13#[derive(Debug, Clone)]
15pub struct SnapOptions {
16 pub snap_to_price: bool,
18 pub snap_to_time: bool,
20 pub snap_distance: f32,
22 pub magnet_mode: bool,
24 pub magnet_distance: f32,
26}
27
28impl Default for SnapOptions {
29 fn default() -> Self {
30 Self {
31 snap_to_price: true,
32 snap_to_time: true,
33 snap_distance: 10.0,
34 magnet_mode: false,
35 magnet_distance: 15.0,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Default)]
42pub struct SnapTargets {
43 pub prices: Vec<f32>,
45 pub times: Vec<f32>,
47 pub drawing_points: Vec<Pos2>,
49}
50
51impl SnapTargets {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn with_price_time(prices: Vec<f32>, times: Vec<f32>) -> Self {
59 Self {
60 prices,
61 times,
62 drawing_points: Vec::new(),
63 }
64 }
65
66 pub fn with_drawing_points(mut self, points: Vec<Pos2>) -> Self {
68 self.drawing_points = points;
69 self
70 }
71
72 pub fn set_prices(&mut self, prices: Vec<f32>) {
74 self.prices = prices;
75 }
76
77 pub fn set_times(&mut self, times: Vec<f32>) {
79 self.times = times;
80 }
81
82 pub fn set_drawing_points(&mut self, points: Vec<Pos2>) {
84 self.drawing_points = points;
85 }
86
87 pub fn clear(&mut self) {
89 self.prices.clear();
90 self.times.clear();
91 self.drawing_points.clear();
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct SnapService {
98 options: SnapOptions,
99}
100
101impl Default for SnapService {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl SnapService {
108 pub fn new() -> Self {
110 Self {
111 options: SnapOptions::default(),
112 }
113 }
114
115 pub fn with_options(options: SnapOptions) -> Self {
117 Self { options }
118 }
119
120 pub fn options(&self) -> &SnapOptions {
122 &self.options
123 }
124
125 pub fn options_mut(&mut self) -> &mut SnapOptions {
127 &mut self.options
128 }
129
130 pub fn snap_point(&self, point: Pos2, targets: &SnapTargets) -> Pos2 {
137 self.snap_point_with_drawing_points(
138 point,
139 &targets.prices,
140 &targets.times,
141 targets.drawing_points.iter().copied(),
142 )
143 }
144
145 pub fn snap_point_with_drawing_points<I>(
153 &self,
154 point: Pos2,
155 prices: &[f32],
156 times: &[f32],
157 drawing_points: I,
158 ) -> Pos2
159 where
160 I: IntoIterator<Item = Pos2>,
161 {
162 let mut result = point;
163
164 if self.options.snap_to_price
166 && !prices.is_empty()
167 && let Some(snapped_y) = self.find_closest(prices, point.y)
168 {
169 result.y = snapped_y;
170 }
171
172 if self.options.snap_to_time
174 && !times.is_empty()
175 && let Some(snapped_x) = self.find_closest(times, point.x)
176 {
177 result.x = snapped_x;
178 }
179
180 if self.options.magnet_mode
182 && let Some(closest_point) = self.find_closest_point(drawing_points, point)
183 {
184 result = closest_point;
185 }
186
187 result
188 }
189
190 fn find_closest(&self, values: &[f32], target: f32) -> Option<f32> {
193 values
194 .iter()
195 .min_by(|a, b| (target - **a).abs().total_cmp(&(target - **b).abs()))
196 .filter(|&&closest| (target - closest).abs() < self.options.snap_distance)
197 .copied()
198 }
199
200 fn find_closest_point<I>(&self, points: I, target: Pos2) -> Option<Pos2>
202 where
203 I: IntoIterator<Item = Pos2>,
204 {
205 let mut min_dist = self.options.magnet_distance;
206 let mut closest = None;
207
208 for p in points {
209 let dist = ((target.x - p.x).powi(2) + (target.y - p.y).powi(2)).sqrt();
210 if dist < min_dist {
211 min_dist = dist;
212 closest = Some(p);
213 }
214 }
215
216 closest
217 }
218
219 pub fn would_snap(&self, point: Pos2, targets: &SnapTargets) -> bool {
223 let snapped = self.snap_point(point, targets);
224 (snapped.x - point.x).abs() > 0.001 || (snapped.y - point.y).abs() > 0.001
225 }
226
227 pub fn get_snap_indicator(&self, point: Pos2, targets: &SnapTargets) -> Option<Pos2> {
232 let snapped = self.snap_point(point, targets);
233 if (snapped.x - point.x).abs() > 0.001 || (snapped.y - point.y).abs() > 0.001 {
234 Some(snapped)
235 } else {
236 None
237 }
238 }
239
240 pub fn set_snap_to_price(&mut self, enabled: bool) {
242 self.options.snap_to_price = enabled;
243 }
244
245 pub fn set_snap_to_time(&mut self, enabled: bool) {
247 self.options.snap_to_time = enabled;
248 }
249
250 pub fn set_magnet_mode(&mut self, enabled: bool) {
252 self.options.magnet_mode = enabled;
253 }
254
255 pub fn set_snap_distance(&mut self, distance: f32) {
257 self.options.snap_distance = distance;
258 }
259
260 pub fn set_magnet_distance(&mut self, distance: f32) {
262 self.options.magnet_distance = distance;
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn snap_respects_targets_modes_and_distances() {
272 let defaults = SnapOptions::default();
273 let magnet = SnapOptions {
274 magnet_mode: true,
275 ..defaults.clone()
276 };
277 let disabled = SnapOptions {
278 snap_to_price: false,
279 snap_to_time: false,
280 ..defaults.clone()
281 };
282 let cases = [
284 (
285 defaults.clone(),
286 SnapTargets::with_price_time(vec![100.0, 150.0, 200.0], vec![]),
287 Pos2::new(50.0, 152.0),
288 Pos2::new(50.0, 150.0),
289 ),
290 (
291 defaults.clone(),
292 SnapTargets::with_price_time(vec![], vec![100.0, 200.0, 300.0]),
293 Pos2::new(203.0, 50.0),
294 Pos2::new(200.0, 50.0),
295 ),
296 (
297 defaults.clone(),
298 SnapTargets::with_price_time(vec![100.0], vec![200.0]),
299 Pos2::new(250.0, 150.0),
300 Pos2::new(250.0, 150.0),
301 ),
302 (
303 magnet.clone(),
304 SnapTargets::new()
305 .with_drawing_points(vec![Pos2::new(100.0, 100.0), Pos2::new(200.0, 200.0)]),
306 Pos2::new(105.0, 103.0),
307 Pos2::new(100.0, 100.0),
308 ),
309 (
310 SnapOptions {
311 magnet_distance: 10.0,
312 ..magnet
313 },
314 SnapTargets::new().with_drawing_points(vec![Pos2::new(100.0, 100.0)]),
315 Pos2::new(150.0, 150.0),
316 Pos2::new(150.0, 150.0),
317 ),
318 (
319 disabled,
320 SnapTargets::with_price_time(vec![100.0], vec![100.0]),
321 Pos2::new(101.0, 101.0),
322 Pos2::new(101.0, 101.0),
323 ),
324 (
325 defaults.clone(),
326 SnapTargets::with_price_time(vec![10.0, f32::NAN], vec![]),
327 Pos2::new(0.0, 10.5),
328 Pos2::new(0.0, 10.0),
329 ),
330 (
331 defaults.clone(),
332 SnapTargets::with_price_time(vec![f32::NAN, 10.0], vec![]),
333 Pos2::new(0.0, 10.5),
334 Pos2::new(0.0, 10.0),
335 ),
336 (
337 defaults.clone(),
338 SnapTargets::with_price_time(vec![10.0, 11.0], vec![]),
339 Pos2::new(0.0, f32::NAN),
340 Pos2::new(0.0, f32::NAN),
341 ),
342 (
343 defaults,
344 SnapTargets::with_price_time(vec![f32::INFINITY], vec![]),
345 Pos2::new(0.0, 10.5),
346 Pos2::new(0.0, 10.5),
347 ),
348 ];
349 for (i, (options, targets, point, expected)) in cases.into_iter().enumerate() {
350 let snapped = SnapService::with_options(options).snap_point(point, &targets);
351 for (got, want) in [(snapped.x, expected.x), (snapped.y, expected.y)] {
352 assert!(
353 got == want || (got.is_nan() && want.is_nan()),
354 "case {i}: {point:?}, {targets:?}: got {snapped:?}, want {expected:?}"
355 );
356 }
357 }
358 }
359
360 #[test]
361 fn test_would_snap() {
362 let service = SnapService::new();
363 let targets = SnapTargets::with_price_time(vec![100.0], vec![]);
364
365 assert!(service.would_snap(Pos2::new(50.0, 102.0), &targets));
366 assert!(!service.would_snap(Pos2::new(50.0, 150.0), &targets));
367 }
368
369 #[test]
370 fn test_snap_indicator() {
371 let service = SnapService::new();
372 let targets = SnapTargets::with_price_time(vec![100.0], vec![]);
373
374 let indicator = service.get_snap_indicator(Pos2::new(50.0, 102.0), &targets);
376 assert!(indicator.is_some());
377 assert_eq!(indicator.unwrap().y, 100.0);
378
379 let indicator = service.get_snap_indicator(Pos2::new(50.0, 150.0), &targets);
381 assert!(indicator.is_none());
382 }
383}