1use crate::drawings::domain::{Drawing, DrawingToolType, HandlePos};
15use crate::drawings::validation::MAX_IMAGE_SIDE;
16use crate::styles::typography::font_size;
17use crate::theme::ThemeExt;
18use egui::{Color32, Pos2};
19
20#[derive(Clone, Debug)]
22pub struct HandleConfig {
23 pub size: f32,
25 pub color: [u8; 4],
28 pub selected_color: [u8; 4],
31 pub show_handles: bool,
33}
34
35impl Default for HandleConfig {
36 fn default() -> Self {
37 Self {
38 size: 8.0,
39 color: Color32::PLACEHOLDER.to_array(),
40 selected_color: Color32::PLACEHOLDER.to_array(),
41 show_handles: true,
42 }
43 }
44}
45
46pub struct HandleService;
52
53impl HandleService {
54 pub fn get_handles(drawing: &Drawing) -> Vec<(HandlePos, Pos2)> {
67 let mut handles = Vec::new();
68
69 match drawing.tool_type {
70 DrawingToolType::TrendLine | DrawingToolType::Measure => {
71 if !drawing.points.is_empty() {
72 handles.push((HandlePos::Start, drawing.points[0]));
73 }
74 if drawing.points.len() >= 2 {
75 handles.push((HandlePos::End, drawing.points[1]));
76 }
77 }
78 DrawingToolType::Rect | DrawingToolType::GannBox => {
79 if drawing.points.len() >= 2 {
80 let p1 = drawing.points[0];
81 let p2 = drawing.points[1];
82 handles.push((HandlePos::TopLeft, p1));
83 handles.push((HandlePos::TopRight, Pos2::new(p2.x, p1.y)));
84 handles.push((HandlePos::BottomLeft, Pos2::new(p1.x, p2.y)));
85 handles.push((HandlePos::BottomRight, p2));
86 let mid = p1 + (p2 - p1) / 2.0;
87 handles.push((HandlePos::Top, Pos2::new(mid.x, p1.y)));
88 handles.push((HandlePos::Bottom, Pos2::new(mid.x, p2.y)));
89 handles.push((HandlePos::Left, Pos2::new(p1.x, mid.y)));
90 handles.push((HandlePos::Right, Pos2::new(p2.x, mid.y)));
91 }
92 }
93 DrawingToolType::FontIcon | DrawingToolType::Sticker => {
94 if let Some(¢er) = drawing.points.first() {
95 let rect = egui::Rect::from_center_size(
96 center,
97 egui::Vec2::splat(font_size(drawing.font_size)),
98 );
99 handles.push((HandlePos::TopLeft, rect.left_top()));
100 handles.push((HandlePos::TopRight, rect.right_top()));
101 handles.push((HandlePos::BottomLeft, rect.left_bottom()));
102 handles.push((HandlePos::BottomRight, rect.right_bottom()));
103 handles.push((HandlePos::Middle, center));
104 }
105 }
106 DrawingToolType::HorizontalLine => {
107 if !drawing.points.is_empty() {
108 handles.push((HandlePos::Start, drawing.points[0]));
109 }
110 }
111 DrawingToolType::VerticalLine => {
112 if !drawing.points.is_empty() {
113 handles.push((HandlePos::Start, drawing.points[0]));
114 }
115 }
116 DrawingToolType::FibonacciRetracement
118 | DrawingToolType::GannFan
119 | DrawingToolType::GannSquare => {
120 if !drawing.points.is_empty() {
121 handles.push((HandlePos::Start, drawing.points[0]));
122 }
123 if drawing.points.len() >= 2 {
124 handles.push((HandlePos::End, drawing.points[1]));
125 }
126 }
127 DrawingToolType::LongPos | DrawingToolType::ShortPos => {
130 if let &[entry, target, ..] = drawing.points.as_slice() {
131 let right = Drawing::position_right(entry, target);
132 handles.push((HandlePos::Start, entry));
133 handles.push((HandlePos::End, Pos2::new(right, entry.y)));
134 handles.push((HandlePos::Point(1), Pos2::new(entry.x, target.y)));
135 if let Some(stop) = drawing.points.get(2) {
136 handles.push((HandlePos::Point(2), Pos2::new(entry.x, stop.y)));
137 }
138 handles.push((
139 HandlePos::Middle,
140 Pos2::new((entry.x + right) / 2.0, entry.y),
141 ));
142 } else if let Some(&entry) = drawing.points.first() {
143 handles.push((HandlePos::Start, entry));
144 }
145 }
146 DrawingToolType::ParallelChannel
147 | DrawingToolType::FlatTopBottom
148 | DrawingToolType::DisjointChannel => {
149 for (i, &point) in drawing.points.iter().take(2).enumerate() {
150 handles.push((HandlePos::Point(i), point));
151 }
152 if let Some(ends) = drawing.channel_second_line() {
153 for (i, point) in ends.into_iter().enumerate() {
154 handles.push((HandlePos::Point(i + 2), point));
155 }
156 }
157 }
158 DrawingToolType::RegressionTrend => {
159 for (i, &point) in drawing.points.iter().take(2).enumerate() {
160 handles.push((HandlePos::Point(i), point));
161 }
162 }
163 DrawingToolType::Brush | DrawingToolType::Highlighter => {
165 if let Some(&point) = drawing.points.get(drawing.points.len() / 2) {
166 handles.push((HandlePos::Middle, point));
167 }
168 }
169 DrawingToolType::BarsPattern if drawing.points.len() >= 3 => {
171 let [a, b, c] = [drawing.points[0], drawing.points[1], drawing.points[2]];
172 handles.push((HandlePos::Start, c));
173 handles.push((HandlePos::End, Pos2::new(c.x + (b.x - a.x).abs(), c.y)));
174 }
175 DrawingToolType::Image => {
176 if let Some(frame) = drawing.image_frame() {
177 handles.push((HandlePos::BottomRight, frame.max));
178 }
179 }
180 _ => {
182 for (i, &point) in drawing.points.iter().enumerate() {
183 handles.push((HandlePos::Point(i), point));
184 }
185 }
186 }
187
188 handles
189 }
190
191 pub fn hit_test_handle(drawing: &Drawing, point: Pos2, handle_size: f32) -> Option<HandlePos> {
196 let hit_radius = handle_size * 1.5;
197 Self::get_handles(drawing)
200 .into_iter()
201 .map(|(pos, handle_point)| (pos, point.distance(handle_point)))
202 .filter(|&(_, dist)| dist <= hit_radius)
203 .min_by(|a, b| a.1.total_cmp(&b.1))
204 .map(|(pos, _)| pos)
205 }
206
207 pub fn update_handle<F, G, H>(
215 drawing: &mut Drawing,
216 handle: HandlePos,
217 new_pos: Pos2,
218 x_to_bar: F,
219 y_to_price: G,
220 translate_price: H,
221 ) where
222 F: Fn(f32) -> f32,
223 G: Fn(f32) -> f64,
224 H: Fn(f64, f32, f32) -> f64,
225 {
226 if handle == HandlePos::Middle {
227 let first = drawing.first_moved_anchor();
228 let count = drawing.points.len().saturating_sub(first).max(1) as f32;
229 let centroid = drawing
230 .points
231 .iter()
232 .skip(first)
233 .fold(egui::Vec2::ZERO, |sum, p| sum + p.to_vec2())
234 / count;
235 let center = Self::get_handles(drawing)
236 .into_iter()
237 .find(|(h, _)| *h == handle)
238 .map_or(Pos2::ZERO + centroid, |(_, p)| p);
239 if !drawing.points.is_empty() {
240 let bars = x_to_bar(new_pos.x) - x_to_bar(center.x);
241 for point in drawing.chart_points.iter_mut().skip(first) {
242 point.bar_idx += bars;
243 point.price = translate_price(point.price, center.y, new_pos.y);
244 }
245 for point in drawing.points.iter_mut().skip(first) {
246 *point += new_pos - center;
247 }
248 }
249 return;
250 }
251 match drawing.tool_type {
252 DrawingToolType::Image => {
253 let Some(frame) = drawing.image_frame() else {
254 return;
255 };
256 let (origin, size) = (frame.min, frame.size());
257 let scale = ((new_pos.x - origin.x) / size.x).max((new_pos.y - origin.y) / size.y);
258 let low = (16.0 / size.min_elem()).min(1.0);
260 let high = MAX_IMAGE_SIDE / size.max_elem();
261 let scale = if scale.is_finite() {
262 scale.clamp(low, high)
263 } else {
264 1.0
265 };
266 drawing.image.get_or_insert_with(Default::default).size =
268 Some((size * scale).min(egui::Vec2::splat(MAX_IMAGE_SIDE)).into());
269 }
270 DrawingToolType::BarsPattern
274 if drawing.points.len() >= 3 && drawing.chart_points.len() >= 3 =>
275 {
276 let Some((_, current)) = Self::get_handles(drawing)
277 .into_iter()
278 .find(|(h, _)| *h == handle)
279 else {
280 return;
281 };
282 let left = usize::from(drawing.points[1].x < drawing.points[0].x);
283 let moved: &[usize] = match handle {
284 HandlePos::Start => &[left, 2],
285 HandlePos::End => &[1 - left],
286 _ => return,
287 };
288 let bars = x_to_bar(new_pos.x) - x_to_bar(current.x);
289 for &i in moved {
290 drawing.points[i].x += new_pos.x - current.x;
291 drawing.chart_points[i].bar_idx += bars;
292 }
293 }
294 DrawingToolType::TrendLine
295 | DrawingToolType::Measure
296 | DrawingToolType::FibonacciRetracement
297 | DrawingToolType::GannFan
298 | DrawingToolType::GannSquare => {
299 Self::update_two_point_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
300 }
301 DrawingToolType::Rect | DrawingToolType::GannBox => {
302 Self::update_rect_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
303 }
304 DrawingToolType::FontIcon | DrawingToolType::Sticker
305 if matches!(
306 handle,
307 HandlePos::TopLeft
308 | HandlePos::TopRight
309 | HandlePos::BottomLeft
310 | HandlePos::BottomRight
311 ) =>
312 {
313 if let Some(¢er) = drawing.points.first() {
314 drawing.font_size =
316 font_size(center.distance(new_pos) * std::f32::consts::SQRT_2);
317 }
318 }
319 DrawingToolType::HorizontalLine => {
320 if !drawing.points.is_empty() && !drawing.chart_points.is_empty() {
321 drawing.points[0].y = new_pos.y;
322 drawing.chart_points[0].price = y_to_price(new_pos.y);
323 }
324 }
325 DrawingToolType::VerticalLine => {
326 if !drawing.points.is_empty() && !drawing.chart_points.is_empty() {
327 drawing.points[0].x = new_pos.x;
328 drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
329 }
330 }
331 DrawingToolType::ParallelChannel
332 | DrawingToolType::FlatTopBottom
333 | DrawingToolType::DisjointChannel => {
334 let HandlePos::Point(index) = handle else {
335 return;
336 };
337 if index < 2 {
338 Self::update_multi_point_handle(
339 drawing,
340 handle,
341 new_pos,
342 &x_to_bar,
343 &y_to_price,
344 );
345 } else if index < 4 && drawing.chart_points.len() >= 3 {
346 let Some(ends) = drawing.channel_second_line() else {
347 return;
348 };
349 let delta = new_pos - ends[index - 2];
350 match drawing.tool_type {
351 DrawingToolType::DisjointChannel => {
352 let base = drawing.points[index - 2];
354 drawing.points[index - 2] = Pos2::new(new_pos.x, base.y - delta.y);
355 let anchor = &mut drawing.chart_points[index - 2];
356 anchor.bar_idx = x_to_bar(new_pos.x);
357 anchor.price = translate_price(anchor.price, base.y, base.y - delta.y);
358 Self::update_multi_point_handle(
359 drawing,
360 HandlePos::Point(2),
361 new_pos,
362 &x_to_bar,
363 &y_to_price,
364 );
365 }
366 DrawingToolType::ParallelChannel
367 if (drawing.points[1].x - drawing.points[0].x).abs() < 1e-3 =>
368 {
369 drawing.points[2].x += delta.x;
370 drawing.chart_points[2].bar_idx = x_to_bar(drawing.points[2].x);
371 }
372 _ => {
373 drawing.points[2].y += delta.y;
374 drawing.chart_points[2].price =
375 if drawing.tool_type == DrawingToolType::ParallelChannel {
376 translate_price(
377 drawing.chart_points[2].price,
378 ends[index - 2].y,
379 new_pos.y,
380 )
381 } else {
382 y_to_price(new_pos.y)
383 };
384 }
385 }
386 }
387 }
388 DrawingToolType::RegressionTrend => {
389 let index = match handle {
390 HandlePos::Start | HandlePos::Point(0) => 0,
391 HandlePos::End | HandlePos::Point(1) => 1,
392 _ => return,
393 };
394 if let (Some(point), Some(anchor)) = (
395 drawing.points.get_mut(index),
396 drawing.chart_points.get_mut(index),
397 ) {
398 point.x = new_pos.x;
399 anchor.bar_idx = x_to_bar(new_pos.x);
400 }
401 }
402 DrawingToolType::TimeCycles | DrawingToolType::Signpost => {
403 match handle {
404 HandlePos::Start | HandlePos::Point(0) => {
405 if let (Some(point), Some(anchor)) =
406 (drawing.points.first_mut(), drawing.chart_points.first_mut())
407 {
408 *point = new_pos;
409 anchor.bar_idx = x_to_bar(new_pos.x);
410 anchor.price = y_to_price(new_pos.y);
411 }
412 }
413 HandlePos::End | HandlePos::Point(1)
414 if drawing.points.len() > 1 && drawing.chart_points.len() > 1 =>
415 {
416 if drawing.tool_type == DrawingToolType::TimeCycles {
417 drawing.points[1].x = new_pos.x;
418 drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
419 } else {
420 drawing.points[1].y = new_pos.y;
421 drawing.chart_points[1].price = y_to_price(new_pos.y);
422 }
423 }
424 _ => return,
425 }
426 drawing.align_second_anchor();
427 }
428 DrawingToolType::LongPos | DrawingToolType::ShortPos => match handle {
431 HandlePos::Point(level @ (1 | 2))
432 if level < drawing.points.len() && level < drawing.chart_points.len() =>
433 {
434 drawing.points[level].y = new_pos.y;
435 drawing.chart_points[level].price = y_to_price(new_pos.y);
436 }
437 HandlePos::End if drawing.points.len() > 1 && drawing.chart_points.len() > 1 => {
438 let x = new_pos.x.max(drawing.points[0].x);
439 for level in 1..drawing.points.len().min(drawing.chart_points.len()) {
440 drawing.points[level].x = x;
441 drawing.chart_points[level].bar_idx = x_to_bar(x);
442 }
443 }
444 _ => Self::update_multi_point_handle(
445 drawing,
446 handle,
447 new_pos,
448 &x_to_bar,
449 &y_to_price,
450 ),
451 },
452 _ => {
453 Self::update_multi_point_handle(drawing, handle, new_pos, &x_to_bar, &y_to_price);
454 }
455 }
456 }
457
458 fn update_two_point_handle<F, G>(
460 drawing: &mut Drawing,
461 handle: HandlePos,
462 new_pos: Pos2,
463 x_to_bar: &F,
464 y_to_price: &G,
465 ) where
466 F: Fn(f32) -> f32,
467 G: Fn(f32) -> f64,
468 {
469 match handle {
470 HandlePos::Start if !drawing.points.is_empty() => {
471 drawing.points[0] = new_pos;
472 if !drawing.chart_points.is_empty() {
473 drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
474 drawing.chart_points[0].price = y_to_price(new_pos.y);
475 }
476 }
477 HandlePos::End if drawing.points.len() >= 2 => {
478 drawing.points[1] = new_pos;
479 if drawing.chart_points.len() >= 2 {
480 drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
481 drawing.chart_points[1].price = y_to_price(new_pos.y);
482 }
483 }
484 _ => {}
485 }
486 }
487
488 fn update_rect_handle<F, G>(
490 drawing: &mut Drawing,
491 handle: HandlePos,
492 new_pos: Pos2,
493 x_to_bar: &F,
494 y_to_price: &G,
495 ) where
496 F: Fn(f32) -> f32,
497 G: Fn(f32) -> f64,
498 {
499 if drawing.points.len() < 2 || drawing.chart_points.len() < 2 {
500 return;
501 }
502
503 match handle {
504 HandlePos::TopLeft => {
505 drawing.points[0] = new_pos;
506 drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
507 drawing.chart_points[0].price = y_to_price(new_pos.y);
508 }
509 HandlePos::TopRight => {
510 drawing.points[0].y = new_pos.y;
511 drawing.points[1].x = new_pos.x;
512 drawing.chart_points[0].price = y_to_price(new_pos.y);
513 drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
514 }
515 HandlePos::BottomLeft => {
516 drawing.points[0].x = new_pos.x;
517 drawing.points[1].y = new_pos.y;
518 drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
519 drawing.chart_points[1].price = y_to_price(new_pos.y);
520 }
521 HandlePos::BottomRight => {
522 drawing.points[1] = new_pos;
523 drawing.chart_points[1].bar_idx = x_to_bar(new_pos.x);
524 drawing.chart_points[1].price = y_to_price(new_pos.y);
525 }
526 HandlePos::Top | HandlePos::Bottom => {
527 let index = usize::from(handle == HandlePos::Bottom);
528 drawing.points[index].y = new_pos.y;
529 drawing.chart_points[index].price = y_to_price(new_pos.y);
530 }
531 HandlePos::Left | HandlePos::Right => {
532 let index = usize::from(handle == HandlePos::Right);
533 drawing.points[index].x = new_pos.x;
534 drawing.chart_points[index].bar_idx = x_to_bar(new_pos.x);
535 }
536 _ => {}
537 }
538 }
539
540 fn update_multi_point_handle<F, G>(
542 drawing: &mut Drawing,
543 handle: HandlePos,
544 new_pos: Pos2,
545 x_to_bar: &F,
546 y_to_price: &G,
547 ) where
548 F: Fn(f32) -> f32,
549 G: Fn(f32) -> f64,
550 {
551 match handle {
552 HandlePos::Point(idx)
553 if idx < drawing.points.len() && idx < drawing.chart_points.len() =>
554 {
555 drawing.points[idx] = new_pos;
556 drawing.chart_points[idx].bar_idx = x_to_bar(new_pos.x);
557 drawing.chart_points[idx].price = y_to_price(new_pos.y);
558 }
559 HandlePos::Start if !drawing.points.is_empty() && !drawing.chart_points.is_empty() => {
560 drawing.points[0] = new_pos;
561 drawing.chart_points[0].bar_idx = x_to_bar(new_pos.x);
562 drawing.chart_points[0].price = y_to_price(new_pos.y);
563 }
564 HandlePos::End if drawing.points.len() >= 2 && drawing.chart_points.len() >= 2 => {
565 let last = drawing.points.len() - 1;
566 drawing.points[last] = new_pos;
567 drawing.chart_points[last].bar_idx = x_to_bar(new_pos.x);
568 drawing.chart_points[last].price = y_to_price(new_pos.y);
569 }
570 _ => {}
571 }
572 }
573
574 pub fn render_handles(
580 painter: &egui::Painter,
581 drawing: &Drawing,
582 config: &HandleConfig,
583 dragging_handle: Option<HandlePos>,
584 ) {
585 if !config.show_handles {
586 return;
587 }
588
589 let theme = painter.ctx().active_theme();
590 let color = |[r, g, b, a]: [u8; 4]| {
591 if [r, g, b, a] == Color32::PLACEHOLDER.to_array() {
592 theme.ui.accent
593 } else {
594 Color32::from_rgba_unmultiplied(r, g, b, a)
595 }
596 };
597 let radius = config.size / 2.0;
598 let glyph = matches!(
599 drawing.tool_type,
600 DrawingToolType::FontIcon | DrawingToolType::Sticker
601 );
602 if glyph && let Some(¢er) = drawing.points.first() {
603 painter.rect_stroke(
604 egui::Rect::from_center_size(
605 center,
606 egui::Vec2::splat(font_size(drawing.font_size)),
607 ),
608 0.0,
609 egui::Stroke::new(1.0, color(config.color)),
610 egui::StrokeKind::Outside,
611 );
612 }
613
614 let position = matches!(
615 drawing.tool_type,
616 DrawingToolType::LongPos | DrawingToolType::ShortPos
617 );
618 for (pos, point) in Self::get_handles(drawing) {
619 if (position || glyph) && pos == HandlePos::Middle {
621 continue;
622 }
623 let ring = if dragging_handle == Some(pos) {
624 None
625 } else {
626 Some(theme.chart.bg)
627 };
628 let mut fill = color(if ring.is_some() {
629 config.color
630 } else {
631 config.selected_color
632 });
633 if position && ring.is_some() {
634 fill = fill.lerp_to_gamma(theme.chart.bg, 0.15);
635 }
636 if position && matches!(pos, HandlePos::Point(_)) {
637 let square = egui::Rect::from_center_size(point, egui::Vec2::splat(2.0 * radius));
638 let corner = radius / 2.0;
639 painter.rect(
640 square,
641 corner,
642 ring.unwrap_or(fill),
643 egui::Stroke::new(1.0, fill),
644 egui::StrokeKind::Inside,
645 );
646 } else if position {
647 painter.circle(
648 point,
649 radius - 0.5,
650 ring.unwrap_or(fill),
651 egui::Stroke::new(1.0, fill),
652 );
653 } else {
654 painter.circle_filled(point, radius, fill);
655 if let Some(bg) = ring {
656 painter.circle_filled(point, radius - RING_WIDTH, bg);
657 }
658 }
659 }
660 }
661}
662
663const RING_WIDTH: f32 = 2.0;
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use crate::drawings::domain::ChartPoint;
670 use egui::pos2;
671
672 fn x_to_bar(x: f32) -> f32 {
675 x / 10.0
676 }
677
678 fn y_to_price(y: f32) -> f64 {
679 1000.0 - y as f64
680 }
681
682 fn create_test_drawing(tool_type: DrawingToolType, points: Vec<Pos2>) -> Drawing {
683 let mut drawing = Drawing::new(1, tool_type);
684 drawing.chart_points = points
685 .iter()
686 .map(|p| ChartPoint::new(x_to_bar(p.x), y_to_price(p.y)))
687 .collect();
688 drawing.points = points;
689 drawing
690 }
691
692 #[test]
693 fn get_handles_by_tool() {
694 use HandlePos::*;
695 let cases = [
696 (
697 DrawingToolType::BarsPattern,
698 vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
699 vec![(Start, pos2(500.0, 100.0)), (End, pos2(700.0, 100.0))],
700 None,
701 ),
702 (
703 DrawingToolType::FontIcon,
704 vec![pos2(100.0, 200.0)],
705 vec![
706 (TopLeft, pos2(80.0, 180.0)),
707 (TopRight, pos2(120.0, 180.0)),
708 (BottomLeft, pos2(80.0, 220.0)),
709 (BottomRight, pos2(120.0, 220.0)),
710 (Middle, pos2(100.0, 200.0)),
711 ],
712 None,
713 ),
714 (
715 DrawingToolType::Sticker,
716 vec![pos2(100.0, 200.0)],
717 vec![
718 (TopLeft, pos2(45.0, 145.0)),
719 (TopRight, pos2(155.0, 145.0)),
720 (BottomLeft, pos2(45.0, 255.0)),
721 (BottomRight, pos2(155.0, 255.0)),
722 (Middle, pos2(100.0, 200.0)),
723 ],
724 None,
725 ),
726 (
727 DrawingToolType::ParallelChannel,
728 vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
729 vec![
730 (Point(0), pos2(100.0, 200.0)),
731 (Point(1), pos2(300.0, 400.0)),
732 (Point(2), pos2(100.0, 100.0)),
733 (Point(3), pos2(300.0, 300.0)),
734 ],
735 None,
736 ),
737 (
738 DrawingToolType::FlatTopBottom,
739 vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
740 vec![
741 (Point(0), pos2(100.0, 200.0)),
742 (Point(1), pos2(300.0, 400.0)),
743 (Point(2), pos2(100.0, 200.0)),
744 (Point(3), pos2(300.0, 200.0)),
745 ],
746 None,
747 ),
748 (
749 DrawingToolType::DisjointChannel,
750 vec![pos2(100.0, 200.0), pos2(300.0, 400.0), pos2(200.0, 200.0)],
751 vec![
752 (Point(0), pos2(100.0, 200.0)),
753 (Point(1), pos2(300.0, 400.0)),
754 (Point(2), pos2(100.0, 300.0)),
755 (Point(3), pos2(300.0, 100.0)),
756 ],
757 None,
758 ),
759 (
760 DrawingToolType::TrendLine,
761 vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
762 vec![(Start, pos2(100.0, 200.0)), (End, pos2(300.0, 400.0))],
763 None,
764 ),
765 (
766 DrawingToolType::Rect,
767 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
768 vec![
769 (TopLeft, pos2(100.0, 100.0)),
770 (TopRight, pos2(200.0, 100.0)),
771 (BottomLeft, pos2(100.0, 300.0)),
772 (BottomRight, pos2(200.0, 300.0)),
773 (Top, pos2(150.0, 100.0)),
774 (Bottom, pos2(150.0, 300.0)),
775 (Left, pos2(100.0, 200.0)),
776 (Right, pos2(200.0, 200.0)),
777 ],
778 None,
779 ),
780 (
781 DrawingToolType::GannBox,
782 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
783 vec![
784 (TopLeft, pos2(100.0, 100.0)),
785 (TopRight, pos2(200.0, 100.0)),
786 (BottomLeft, pos2(100.0, 300.0)),
787 (BottomRight, pos2(200.0, 300.0)),
788 (Top, pos2(150.0, 100.0)),
789 (Bottom, pos2(150.0, 300.0)),
790 (Left, pos2(100.0, 200.0)),
791 (Right, pos2(200.0, 200.0)),
792 ],
793 None,
794 ),
795 (
796 DrawingToolType::LongPos,
797 vec![pos2(100.0, 200.0), pos2(400.0, 100.0), pos2(400.0, 250.0)],
798 vec![
799 (Start, pos2(100.0, 200.0)),
800 (End, pos2(400.0, 200.0)),
801 (Point(1), pos2(100.0, 100.0)),
802 (Point(2), pos2(100.0, 250.0)),
803 (Middle, pos2(250.0, 200.0)),
804 ],
805 None,
806 ),
807 (
809 DrawingToolType::ShortPos,
810 vec![pos2(100.0, 200.0), pos2(150.0, 250.0), pos2(150.0, 120.0)],
811 vec![
812 (Start, pos2(100.0, 200.0)),
813 (End, pos2(300.0, 200.0)),
814 (Point(1), pos2(100.0, 250.0)),
815 (Point(2), pos2(100.0, 120.0)),
816 (Middle, pos2(200.0, 200.0)),
817 ],
818 None,
819 ),
820 (
821 DrawingToolType::HorizontalLine,
822 vec![pos2(50.0, 120.0)],
823 vec![(Start, pos2(50.0, 120.0))],
824 None,
825 ),
826 (
827 DrawingToolType::FibonacciRetracement,
828 vec![pos2(0.0, 0.0), pos2(100.0, 50.0)],
829 vec![(Start, pos2(0.0, 0.0)), (End, pos2(100.0, 50.0))],
830 None,
831 ),
832 (
833 DrawingToolType::FibonacciChannel,
834 vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
835 vec![
836 (Point(0), pos2(0.0, 0.0)),
837 (Point(1), pos2(30.0, 0.0)),
838 (Point(2), pos2(0.0, 60.0)),
839 ],
840 None,
841 ),
842 (
843 DrawingToolType::Triangle,
844 vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
845 vec![
846 (Point(0), pos2(0.0, 0.0)),
847 (Point(1), pos2(30.0, 0.0)),
848 (Point(2), pos2(0.0, 60.0)),
849 ],
850 None,
851 ),
852 (
853 DrawingToolType::Brush,
854 vec![
855 pos2(0.0, 0.0),
856 pos2(30.0, 20.0),
857 pos2(60.0, 40.0),
858 pos2(90.0, 60.0),
859 pos2(120.0, 80.0),
860 ],
861 vec![(Middle, pos2(60.0, 40.0))],
862 None,
863 ),
864 (
865 DrawingToolType::Highlighter,
866 vec![pos2(0.0, 0.0), pos2(30.0, 20.0)],
867 vec![(Middle, pos2(30.0, 20.0))],
868 None,
869 ),
870 (
871 DrawingToolType::Image,
872 vec![pos2(100.0, 200.0)],
873 vec![(BottomRight, pos2(220.0, 290.0))],
874 None,
875 ),
876 (
877 DrawingToolType::Image,
878 vec![pos2(100.0, 200.0)],
879 vec![(BottomRight, pos2(300.0, 300.0))],
880 Some([200.0, 100.0]),
881 ),
882 ];
883 for (tool, points, expected, image_size) in cases {
884 let mut drawing = create_test_drawing(tool, points);
885 if let Some(size) = image_size {
886 drawing.image = Some(crate::drawings::ImageConfig {
887 size: Some(size),
888 ..Default::default()
889 });
890 }
891 assert_eq!(HandleService::get_handles(&drawing), expected, "{tool:?}");
892 }
893 let anchor_counts: &[(&[DrawingToolType], usize)] = &[
895 (
896 &[
897 DrawingToolType::CrossCursor,
898 DrawingToolType::DotCursor,
899 DrawingToolType::ArrowCursor,
900 DrawingToolType::Eraser,
901 ],
902 0,
903 ),
904 (
905 &[
906 DrawingToolType::HorizontalLine,
907 DrawingToolType::HorizontalRay,
908 DrawingToolType::VerticalLine,
909 DrawingToolType::CrossLine,
910 DrawingToolType::TextLabel,
911 DrawingToolType::AnchoredText,
912 DrawingToolType::AnchoredNote,
913 DrawingToolType::PriceLabel,
914 DrawingToolType::FlagNote,
915 DrawingToolType::ArrowMarkUp,
916 DrawingToolType::ArrowMarkDown,
917 DrawingToolType::AnchoredVWAP,
918 DrawingToolType::AnchoredVolumeProfile,
919 DrawingToolType::Table,
920 DrawingToolType::Comment,
921 DrawingToolType::FontIcon,
922 DrawingToolType::Tweet,
923 DrawingToolType::Idea,
924 ],
925 1,
926 ),
927 (
928 &[
929 DrawingToolType::TrendLine,
930 DrawingToolType::Ray,
931 DrawingToolType::InfoLine,
932 DrawingToolType::ExtendedLine,
933 DrawingToolType::TrendAngle,
934 DrawingToolType::RegressionTrend,
935 DrawingToolType::FibonacciRetracement,
936 DrawingToolType::FibonacciTimeZones,
937 DrawingToolType::FibonacciSpeedFan,
938 DrawingToolType::FibonacciCircles,
939 DrawingToolType::FibonacciSpiral,
940 DrawingToolType::FibonacciSpeedResistanceArcs,
941 DrawingToolType::GannBox,
942 DrawingToolType::GannSquare,
943 DrawingToolType::GannFixed,
944 DrawingToolType::GannFan,
945 DrawingToolType::CyclicLines,
946 DrawingToolType::TimeCycles,
947 DrawingToolType::SineLine,
948 DrawingToolType::Forecast,
949 DrawingToolType::FixedRangeVolumeProfile,
950 DrawingToolType::Measure,
951 DrawingToolType::PriceRange,
952 DrawingToolType::DateRange,
953 DrawingToolType::DateAndPriceRange,
954 DrawingToolType::ArrowMarker,
955 DrawingToolType::Arrow,
956 DrawingToolType::Rect,
957 DrawingToolType::Circle,
958 DrawingToolType::Ellipse,
959 DrawingToolType::Note,
960 DrawingToolType::Signpost,
961 DrawingToolType::PriceNote,
962 DrawingToolType::Callout,
963 ],
964 2,
965 ),
966 (
967 &[
968 DrawingToolType::Pitchfork,
969 DrawingToolType::SchiffPitchfork,
970 DrawingToolType::ModifiedSchiffPitchfork,
971 DrawingToolType::InsidePitchfork,
972 DrawingToolType::FibonacciExtension,
973 DrawingToolType::FibonacciChannel,
974 DrawingToolType::FibonacciWedge,
975 DrawingToolType::TrendBasedFibTime,
976 DrawingToolType::Pitchfan,
977 DrawingToolType::Triangle,
978 DrawingToolType::RotatedRect,
979 DrawingToolType::Arc,
980 DrawingToolType::Curve,
981 DrawingToolType::ProjectionTool,
982 DrawingToolType::GhostFeed,
983 DrawingToolType::ElliottDoubleCombo,
984 ],
985 3,
986 ),
987 (
988 &[
989 DrawingToolType::ABCDPattern,
990 DrawingToolType::ElliottCorrection,
991 DrawingToolType::TrianglePattern,
992 DrawingToolType::DoubleCurve,
993 ],
994 4,
995 ),
996 (
997 &[
998 DrawingToolType::XABCDPattern,
999 DrawingToolType::CypherPattern,
1000 DrawingToolType::ElliottTripleCombo,
1001 DrawingToolType::Polyline,
1002 DrawingToolType::Path,
1003 ],
1004 5,
1005 ),
1006 (
1007 &[
1008 DrawingToolType::ElliottImpulse,
1009 DrawingToolType::ElliottTriangle,
1010 ],
1011 6,
1012 ),
1013 (
1014 &[
1015 DrawingToolType::HeadAndShoulders,
1016 DrawingToolType::ThreeDrivesPattern,
1017 ],
1018 7,
1019 ),
1020 ];
1021 for &(tools, count) in anchor_counts {
1022 for &tool in tools {
1023 let points: Vec<_> = (0..count)
1024 .map(|i| pos2(i as f32 * 30.0, i as f32 * 20.0))
1025 .collect();
1026 let drawing = create_test_drawing(tool, points.clone());
1027 let handles = HandleService::get_handles(&drawing);
1028 for point in points {
1029 assert!(
1030 handles.iter().any(|(_, p)| *p == point),
1031 "{tool:?}: no handle at {point:?}"
1032 );
1033 }
1034 }
1035 }
1036 }
1037
1038 #[test]
1041 fn image_corner_drag_keeps_proportions() {
1042 use crate::drawings::{DrawingManager, ImageConfig};
1043 for (size, pointer, expected) in [
1044 (Some([200.0, 100.0]), pos2(500.0, 150.0), [400.0, 200.0]),
1045 (Some([200.0, 100.0]), pos2(150.0, 400.0), [600.0, 300.0]),
1046 (Some([200.0, 100.0]), pos2(90.0, 90.0), [32.0, 16.0]),
1047 (Some([200.0, 100.0]), pos2(1e6, 0.0), [10_000.0, 5000.0]),
1048 (Some([71.0, 20.0]), pos2(1e6, 0.0), [10_000.0, 2816.9014]),
1049 (None, pos2(340.0, 190.0), [240.0, 180.0]),
1050 (Some([240.0, 1.0]), pos2(100.0, 100.0), [240.0, 1.0]),
1051 ] {
1052 let mut manager = DrawingManager::new();
1053 let mut drawing = create_test_drawing(DrawingToolType::Image, vec![pos2(100.0, 100.0)]);
1054 drawing.image = size.map(|size| ImageConfig {
1055 size: Some(size),
1056 opacity: 128,
1057 });
1058 let before = drawing.clone();
1059 manager.drawings.push(drawing);
1060 manager.start_drag_handle(1, HandlePos::BottomRight);
1061 manager.update_drag_handle(pointer, x_to_bar, y_to_price, |p, from, to| {
1062 p + f64::from(from - to)
1063 });
1064 let drawing = &manager.drawings[0];
1065 let actual = drawing.image.unwrap();
1066 for (actual, expected) in actual.size.unwrap().into_iter().zip(expected) {
1067 assert!((actual - expected).abs() < 1e-3, "{size:?} {pointer:?}");
1068 }
1069 assert_eq!(actual.opacity, before.image.map_or(255, |c| c.opacity));
1070 assert_eq!(drawing.points, before.points);
1071 assert_eq!(drawing.chart_points, before.chart_points);
1072 assert!(
1073 crate::drawings::persistence::drawing_to_stored(drawing, |bar| bar as i64 * 60_000)
1074 .invalid_field()
1075 .is_none()
1076 );
1077 manager.end_drag_handle();
1078 if actual != before.image.unwrap_or_default() {
1080 assert!(manager.undo());
1081 assert_eq!(manager.drawings[0].image, before.image);
1082 assert!(!manager.undo());
1083 }
1084 }
1085 }
1086
1087 #[test]
1089 fn glyph_corner_drags_resize_about_the_centre() {
1090 use crate::drawings::DrawingManager;
1091 for tool in [DrawingToolType::FontIcon, DrawingToolType::Sticker] {
1092 for handle in [
1093 HandlePos::TopLeft,
1094 HandlePos::TopRight,
1095 HandlePos::BottomLeft,
1096 HandlePos::BottomRight,
1097 ] {
1098 let mut manager = DrawingManager::new();
1099 let drawing = create_test_drawing(tool, vec![pos2(100.0, 200.0)]);
1100 let (anchors, size_before) = (drawing.chart_points.clone(), drawing.font_size);
1101 manager.drawings.push(drawing);
1102 manager.start_drag_handle(1, handle);
1103 for (point, size) in [
1106 (pos2(70.0, 160.0), 70.710_68),
1107 (pos2(100.0, 200.0), 1.0),
1108 (pos2(1000.0, 2000.0), 256.0),
1109 (pos2(148.0, 248.0), 96.0),
1110 ] {
1111 manager.update_drag_handle(point, x_to_bar, y_to_price, |p, from, to| {
1112 p + f64::from(from - to)
1113 });
1114 let drawing = &manager.drawings[0];
1115 assert!(
1116 (drawing.font_size - size).abs() < 1e-4,
1117 "{tool:?} {handle:?}"
1118 );
1119 assert_eq!(drawing.points, [pos2(100.0, 200.0)]);
1120 assert_eq!(drawing.chart_points, anchors);
1121 assert!(
1122 crate::drawings::persistence::drawing_to_stored(drawing, |bar| {
1123 bar as i64 * 60_000
1124 })
1125 .invalid_field()
1126 .is_none()
1127 );
1128 }
1129 let ctx = egui::Context::default();
1130 let output = ctx.run_ui(egui::RawInput::default(), |_| {
1131 HandleService::render_handles(
1132 &egui::Painter::new(
1133 ctx.clone(),
1134 egui::LayerId::background(),
1135 egui::Rect::EVERYTHING,
1136 ),
1137 &manager.drawings[0],
1138 &HandleConfig::default(),
1139 Some(handle),
1140 );
1141 });
1142 assert!(
1143 output.shapes.iter().any(|s| matches!(&s.shape,
1144 egui::Shape::Rect(r) if r.rect.center() == pos2(100.0, 200.0)
1145 && (r.rect.width() - 96.0).abs() < 1e-4
1146 && (r.rect.height() - 96.0).abs() < 1e-4 && r.stroke.width > 0.0
1147 )),
1148 "selected glyph has a square frame"
1149 );
1150 output.drop_without_applying_deltas();
1151 manager.end_drag_handle();
1152 assert!(manager.undo());
1153 assert_eq!(manager.drawings[0].font_size, size_before);
1154 assert!(!manager.undo(), "one undo for the whole drag");
1155 assert!(manager.redo());
1156 assert!((manager.drawings[0].font_size - 96.0).abs() < 1e-4);
1157 HandleService::update_handle(
1158 &mut manager.drawings[0],
1159 HandlePos::Middle,
1160 pos2(110.0, 220.0),
1161 x_to_bar,
1162 y_to_price,
1163 |p, from, to| p + f64::from(from - to),
1164 );
1165 assert_eq!(manager.drawings[0].points, [pos2(110.0, 220.0)]);
1166 assert_eq!(
1167 manager.drawings[0].chart_points,
1168 [ChartPoint::new(11.0, 780.0)]
1169 );
1170 }
1171 }
1172 }
1173
1174 #[test]
1175 fn test_hit_test_handle() {
1176 let drawing = create_test_drawing(
1177 DrawingToolType::TrendLine,
1178 vec![Pos2::new(100.0, 200.0), Pos2::new(300.0, 400.0)],
1179 );
1180
1181 let hit = HandleService::hit_test_handle(&drawing, Pos2::new(102.0, 202.0), 8.0);
1183 assert_eq!(hit, Some(HandlePos::Start));
1184
1185 let miss = HandleService::hit_test_handle(&drawing, Pos2::new(500.0, 500.0), 8.0);
1187 assert_eq!(miss, None);
1188
1189 let mut glyph = create_test_drawing(DrawingToolType::Sticker, vec![pos2(100.0, 200.0)]);
1191 glyph.font_size = 16.0;
1192 for (point, expected) in [
1193 (pos2(100.0, 200.0), HandlePos::Middle),
1194 (pos2(108.0, 208.0), HandlePos::BottomRight),
1195 ] {
1196 assert_eq!(
1197 HandleService::hit_test_handle(&glyph, point, 8.0),
1198 Some(expected)
1199 );
1200 }
1201 }
1202
1203 #[test]
1208 fn update_handle_moves_screen_and_chart_points() {
1209 let cases = [
1211 (
1212 DrawingToolType::BarsPattern,
1213 vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1214 HandlePos::End,
1215 pos2(750.0, 50.0),
1216 vec![pos2(350.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1217 vec![
1218 ChartPoint::new(35.0, 800.0),
1219 ChartPoint::new(10.0, 600.0),
1220 ChartPoint::new(50.0, 900.0),
1221 ],
1222 ),
1223 (
1224 DrawingToolType::BarsPattern,
1225 vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1226 HandlePos::Start,
1227 pos2(450.0, 150.0),
1228 vec![pos2(300.0, 200.0), pos2(50.0, 400.0), pos2(450.0, 100.0)],
1229 vec![
1230 ChartPoint::new(30.0, 800.0),
1231 ChartPoint::new(5.0, 600.0),
1232 ChartPoint::new(45.0, 900.0),
1233 ],
1234 ),
1235 (
1236 DrawingToolType::BarsPattern,
1237 vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(500.0, 100.0)],
1238 HandlePos::Middle,
1239 pos2(520.0, 90.0),
1240 vec![pos2(300.0, 200.0), pos2(100.0, 400.0), pos2(520.0, 90.0)],
1241 vec![
1242 ChartPoint::new(30.0, 800.0),
1243 ChartPoint::new(10.0, 600.0),
1244 ChartPoint::new(52.0, 910.0),
1245 ],
1246 ),
1247 (
1249 DrawingToolType::Signpost,
1250 vec![pos2(100.0, 300.0), pos2(100.0, 200.0)],
1251 HandlePos::Point(1),
1252 pos2(250.0, 80.0),
1253 vec![pos2(100.0, 300.0), pos2(100.0, 80.0)],
1254 vec![ChartPoint::new(10.0, 700.0), ChartPoint::new(10.0, 920.0)],
1255 ),
1256 (
1257 DrawingToolType::Signpost,
1258 vec![pos2(100.0, 300.0), pos2(100.0, 200.0)],
1259 HandlePos::Point(0),
1260 pos2(150.0, 320.0),
1261 vec![pos2(150.0, 320.0), pos2(150.0, 200.0)],
1262 vec![ChartPoint::new(15.0, 680.0), ChartPoint::new(15.0, 800.0)],
1263 ),
1264 (
1265 DrawingToolType::Rect,
1266 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1267 HandlePos::Top,
1268 pos2(250.0, 80.0),
1269 vec![pos2(100.0, 80.0), pos2(200.0, 300.0)],
1270 vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(20.0, 700.0)],
1271 ),
1272 (
1273 DrawingToolType::Rect,
1274 vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1275 HandlePos::Top,
1276 pos2(250.0, 80.0),
1277 vec![pos2(200.0, 80.0), pos2(100.0, 100.0)],
1278 vec![ChartPoint::new(20.0, 920.0), ChartPoint::new(10.0, 900.0)],
1279 ),
1280 (
1281 DrawingToolType::Rect,
1282 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1283 HandlePos::Bottom,
1284 pos2(250.0, 80.0),
1285 vec![pos2(100.0, 100.0), pos2(200.0, 80.0)],
1286 vec![ChartPoint::new(10.0, 900.0), ChartPoint::new(20.0, 920.0)],
1287 ),
1288 (
1289 DrawingToolType::Rect,
1290 vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1291 HandlePos::Bottom,
1292 pos2(250.0, 80.0),
1293 vec![pos2(200.0, 300.0), pos2(100.0, 80.0)],
1294 vec![ChartPoint::new(20.0, 700.0), ChartPoint::new(10.0, 920.0)],
1295 ),
1296 (
1297 DrawingToolType::Rect,
1298 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1299 HandlePos::Left,
1300 pos2(250.0, 80.0),
1301 vec![pos2(250.0, 100.0), pos2(200.0, 300.0)],
1302 vec![ChartPoint::new(25.0, 900.0), ChartPoint::new(20.0, 700.0)],
1303 ),
1304 (
1305 DrawingToolType::Rect,
1306 vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1307 HandlePos::Left,
1308 pos2(250.0, 80.0),
1309 vec![pos2(250.0, 300.0), pos2(100.0, 100.0)],
1310 vec![ChartPoint::new(25.0, 700.0), ChartPoint::new(10.0, 900.0)],
1311 ),
1312 (
1313 DrawingToolType::Rect,
1314 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1315 HandlePos::Right,
1316 pos2(250.0, 80.0),
1317 vec![pos2(100.0, 100.0), pos2(250.0, 300.0)],
1318 vec![ChartPoint::new(10.0, 900.0), ChartPoint::new(25.0, 700.0)],
1319 ),
1320 (
1321 DrawingToolType::Rect,
1322 vec![pos2(200.0, 300.0), pos2(100.0, 100.0)],
1323 HandlePos::Right,
1324 pos2(250.0, 80.0),
1325 vec![pos2(200.0, 300.0), pos2(250.0, 100.0)],
1326 vec![ChartPoint::new(20.0, 700.0), ChartPoint::new(25.0, 900.0)],
1327 ),
1328 (
1330 DrawingToolType::ParallelChannel,
1331 vec![pos2(100.0, 100.0), pos2(200.0, 200.0), pos2(600.0, 700.0)],
1332 HandlePos::Point(2),
1333 pos2(150.0, 225.0),
1334 vec![pos2(100.0, 100.0), pos2(200.0, 200.0), pos2(600.0, 725.0)],
1335 vec![
1336 ChartPoint::new(10.0, 900.0),
1337 ChartPoint::new(20.0, 800.0),
1338 ChartPoint::new(60.0, 275.0),
1339 ],
1340 ),
1341 (
1342 DrawingToolType::ParallelChannel,
1343 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1344 HandlePos::Point(0),
1345 pos2(150.0, 250.0),
1346 vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1347 vec![
1348 ChartPoint::new(15.0, 750.0),
1349 ChartPoint::new(30.0, 700.0),
1350 ChartPoint::new(20.0, 900.0),
1351 ],
1352 ),
1353 (
1354 DrawingToolType::ParallelChannel,
1355 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1356 HandlePos::Point(1),
1357 pos2(350.0, 250.0),
1358 vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1359 vec![
1360 ChartPoint::new(10.0, 800.0),
1361 ChartPoint::new(35.0, 750.0),
1362 ChartPoint::new(20.0, 900.0),
1363 ],
1364 ),
1365 (
1366 DrawingToolType::ParallelChannel,
1367 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1368 HandlePos::Point(2),
1369 pos2(150.0, 75.0),
1370 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1371 vec![
1372 ChartPoint::new(10.0, 800.0),
1373 ChartPoint::new(30.0, 700.0),
1374 ChartPoint::new(20.0, 875.0),
1375 ],
1376 ),
1377 (
1378 DrawingToolType::ParallelChannel,
1379 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1380 HandlePos::Point(3),
1381 pos2(350.0, 175.0),
1382 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1383 vec![
1384 ChartPoint::new(10.0, 800.0),
1385 ChartPoint::new(30.0, 700.0),
1386 ChartPoint::new(20.0, 875.0),
1387 ],
1388 ),
1389 (
1390 DrawingToolType::FlatTopBottom,
1391 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1392 HandlePos::Point(0),
1393 pos2(150.0, 250.0),
1394 vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1395 vec![
1396 ChartPoint::new(15.0, 750.0),
1397 ChartPoint::new(30.0, 700.0),
1398 ChartPoint::new(20.0, 900.0),
1399 ],
1400 ),
1401 (
1402 DrawingToolType::FlatTopBottom,
1403 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1404 HandlePos::Point(1),
1405 pos2(350.0, 250.0),
1406 vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1407 vec![
1408 ChartPoint::new(10.0, 800.0),
1409 ChartPoint::new(35.0, 750.0),
1410 ChartPoint::new(20.0, 900.0),
1411 ],
1412 ),
1413 (
1414 DrawingToolType::FlatTopBottom,
1415 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1416 HandlePos::Point(2),
1417 pos2(150.0, 125.0),
1418 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1419 vec![
1420 ChartPoint::new(10.0, 800.0),
1421 ChartPoint::new(30.0, 700.0),
1422 ChartPoint::new(20.0, 875.0),
1423 ],
1424 ),
1425 (
1426 DrawingToolType::FlatTopBottom,
1427 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1428 HandlePos::Point(3),
1429 pos2(350.0, 125.0),
1430 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 125.0)],
1431 vec![
1432 ChartPoint::new(10.0, 800.0),
1433 ChartPoint::new(30.0, 700.0),
1434 ChartPoint::new(20.0, 875.0),
1435 ],
1436 ),
1437 (
1438 DrawingToolType::DisjointChannel,
1439 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1440 HandlePos::Point(0),
1441 pos2(150.0, 250.0),
1442 vec![pos2(150.0, 250.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1443 vec![
1444 ChartPoint::new(15.0, 750.0),
1445 ChartPoint::new(30.0, 700.0),
1446 ChartPoint::new(20.0, 900.0),
1447 ],
1448 ),
1449 (
1450 DrawingToolType::DisjointChannel,
1451 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1452 HandlePos::Point(1),
1453 pos2(350.0, 250.0),
1454 vec![pos2(100.0, 200.0), pos2(350.0, 250.0), pos2(200.0, 100.0)],
1455 vec![
1456 ChartPoint::new(10.0, 800.0),
1457 ChartPoint::new(35.0, 750.0),
1458 ChartPoint::new(20.0, 900.0),
1459 ],
1460 ),
1461 (
1462 DrawingToolType::DisjointChannel,
1463 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1464 HandlePos::Point(2),
1465 pos2(150.0, 175.0),
1466 vec![pos2(150.0, 175.0), pos2(300.0, 300.0), pos2(150.0, 175.0)],
1467 vec![
1468 ChartPoint::new(15.0, 825.0),
1469 ChartPoint::new(30.0, 700.0),
1470 ChartPoint::new(15.0, 825.0),
1471 ],
1472 ),
1473 (
1474 DrawingToolType::DisjointChannel,
1475 vec![pos2(100.0, 200.0), pos2(300.0, 300.0), pos2(200.0, 100.0)],
1476 HandlePos::Point(3),
1477 pos2(350.0, 75.0),
1478 vec![pos2(100.0, 200.0), pos2(350.0, 275.0), pos2(350.0, 75.0)],
1479 vec![
1480 ChartPoint::new(10.0, 800.0),
1481 ChartPoint::new(35.0, 725.0),
1482 ChartPoint::new(35.0, 925.0),
1483 ],
1484 ),
1485 (
1486 DrawingToolType::TimeCycles,
1487 vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1488 HandlePos::Point(0),
1489 pos2(150.0, 250.0),
1490 vec![pos2(150.0, 250.0), pos2(300.0, 250.0)],
1491 vec![ChartPoint::new(15.0, 750.0), ChartPoint::new(30.0, 750.0)],
1492 ),
1493 (
1494 DrawingToolType::TimeCycles,
1495 vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1496 HandlePos::Point(1),
1497 pos2(50.0, 250.0),
1498 vec![pos2(100.0, 200.0), pos2(50.0, 200.0)],
1499 vec![ChartPoint::new(10.0, 800.0), ChartPoint::new(5.0, 800.0)],
1500 ),
1501 (
1502 DrawingToolType::TimeCycles,
1503 vec![pos2(100.0, 200.0), pos2(300.0, 200.0)],
1504 HandlePos::Middle,
1505 pos2(250.0, 280.0),
1506 vec![pos2(150.0, 280.0), pos2(350.0, 280.0)],
1507 vec![ChartPoint::new(15.0, 720.0), ChartPoint::new(35.0, 720.0)],
1508 ),
1509 (
1510 DrawingToolType::TrendLine,
1511 vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
1512 HandlePos::Start,
1513 pos2(150.0, 250.0),
1514 vec![pos2(150.0, 250.0), pos2(300.0, 400.0)],
1515 vec![ChartPoint::new(15.0, 750.0), ChartPoint::new(30.0, 600.0)],
1516 ),
1517 (
1518 DrawingToolType::TrendLine,
1519 vec![pos2(100.0, 200.0), pos2(300.0, 400.0)],
1520 HandlePos::Middle,
1521 pos2(250.0, 280.0),
1522 vec![pos2(150.0, 180.0), pos2(350.0, 380.0)],
1523 vec![ChartPoint::new(15.0, 820.0), ChartPoint::new(35.0, 620.0)],
1524 ),
1525 (
1526 DrawingToolType::Rect,
1527 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1528 HandlePos::TopRight,
1529 pos2(250.0, 80.0),
1530 vec![pos2(100.0, 80.0), pos2(250.0, 300.0)],
1531 vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(25.0, 700.0)],
1532 ),
1533 (
1534 DrawingToolType::GannBox,
1535 vec![pos2(100.0, 100.0), pos2(200.0, 300.0)],
1536 HandlePos::TopRight,
1537 pos2(250.0, 80.0),
1538 vec![pos2(100.0, 80.0), pos2(250.0, 300.0)],
1539 vec![ChartPoint::new(10.0, 920.0), ChartPoint::new(25.0, 700.0)],
1540 ),
1541 (
1542 DrawingToolType::Triangle,
1543 vec![pos2(0.0, 0.0), pos2(30.0, 0.0), pos2(0.0, 60.0)],
1544 HandlePos::Middle,
1545 pos2(20.0, 30.0),
1546 vec![pos2(10.0, 10.0), pos2(40.0, 10.0), pos2(10.0, 70.0)],
1547 vec![
1548 ChartPoint::new(1.0, 990.0),
1549 ChartPoint::new(4.0, 990.0),
1550 ChartPoint::new(1.0, 930.0),
1551 ],
1552 ),
1553 ];
1554 for (tool, points, handle, to, screen, chart) in cases {
1555 for logarithmic in [false, true] {
1556 let price = |y: f32| {
1557 if logarithmic {
1558 1000.0 * (-f64::from(y) / 100.0).exp()
1559 } else {
1560 y_to_price(y)
1561 }
1562 };
1563 let mut drawing = create_test_drawing(tool, points.clone());
1564 for (anchor, point) in drawing.chart_points.iter_mut().zip(&drawing.points) {
1565 anchor.price = price(point.y);
1566 }
1567 HandleService::update_handle(
1568 &mut drawing,
1569 handle,
1570 to,
1571 x_to_bar,
1572 |y: f32| price(y.clamp(0.0, 350.0)),
1573 |p, from, to| {
1574 if logarithmic {
1575 p * (-f64::from(to - from) / 100.0).exp()
1576 } else {
1577 p - f64::from(to - from)
1578 }
1579 },
1580 );
1581 assert_eq!(drawing.points, screen, "{tool:?} {handle:?}");
1582 let expected = if logarithmic {
1583 screen
1584 .iter()
1585 .map(|p| ChartPoint::new(x_to_bar(p.x), price(p.y)))
1586 .collect()
1587 } else {
1588 chart.clone()
1589 };
1590 if logarithmic {
1591 assert_eq!(drawing.chart_points.len(), expected.len());
1592 for (got, want) in drawing.chart_points.iter().zip(&expected) {
1593 assert_eq!(got.bar_idx, want.bar_idx, "{tool:?} {handle:?}");
1594 assert!(
1595 (got.price - want.price).abs() < 1e-12 * want.price.abs(),
1596 "{tool:?} {handle:?}: {got:?}, expected {want:?}"
1597 );
1598 }
1599 } else {
1600 assert_eq!(drawing.chart_points, expected, "{tool:?} {handle:?}");
1601 }
1602 }
1603 }
1604 }
1605}