1use crate::annotation::{Annotatable, AnnotationData};
2use crate::annotation_list::DEFAULT_GROUP_NAME;
3use crate::comment::Comment;
4use crate::config::SurferTheme;
5use crate::displayed_item::DisplayedItemRef;
6use crate::graphics::GraphicsY;
7use crate::message::Message;
8use crate::time::TimeFormatter;
9use crate::{Viewport, view::DrawingContext, wave_data::WaveData};
10
11use chrono::{DateTime, Local};
12use egui::{Id, Pos2, Response, Stroke, Ui, Vec2, Widget};
13use emath::RectTransform;
14use num::BigInt;
15use serde::{Deserialize, Serialize};
16
17const DEFAULT_TYPE: &str = "Arrow";
18const SELECTED_GAMMA_FACTOR: f32 = 1.1;
19const SELECTED_WIDTH_FACTOR: f32 = 1.2;
20const HITBOX_SIZE: f32 = 4.0;
21const HEAD_LEN_FACTOR: f32 = 5.0;
22const HEAD_WIDTH_FACTOR: f32 = 3.0;
23
24#[derive(Clone, Serialize, Deserialize, Debug)]
25pub enum ArrowHeadMode {
26 End, Double, }
29
30#[derive(Clone, Serialize, Deserialize, Debug)]
31pub struct WavePoint {
32 pub time: BigInt,
33 pub attached_item: Option<DisplayedItemRef>,
34 pub screen_pos: Pos2,
35}
36
37#[derive(Clone, Copy, Debug)]
38struct ArrowSegments {
39 shaft_start: Pos2,
40 shaft_end: Pos2,
41 end_tip: Pos2,
42 end_left: Pos2,
43 end_right: Pos2,
44 start_tip: Option<Pos2>,
45 start_left: Option<Pos2>,
46 start_right: Option<Pos2>,
47}
48
49fn distance_to_segment(p: Pos2, a: Pos2, b: Pos2) -> f32 {
51 let ab = b - a;
52 let ap = p - a;
53
54 let ab_len_sq = ab.length_sq();
55 if ab_len_sq <= 0.0001 {
56 return ap.length();
57 }
58
59 let t = (ap.dot(ab) / ab_len_sq).clamp(0.0, 1.0);
60 let closest = a + ab * t;
61 (p - closest).length()
62}
63
64fn arrow_geometry(from: Pos2, to: Pos2, width: f32) -> Option<(Pos2, Pos2, Pos2)> {
66 let v = to - from;
67 let len = v.length();
68
69 if len <= 0.1 {
70 return None;
71 }
72
73 let dir = v / len;
74 let perp = Vec2::new(-dir.y, dir.x);
75
76 let head_len = width * HEAD_LEN_FACTOR;
77 let head_half_width = width * HEAD_WIDTH_FACTOR;
78
79 let base = to - dir * head_len;
80 let left = base + perp * head_half_width;
81 let right = base - perp * head_half_width;
82
83 Some((base, left, right))
84}
85fn item_center_y(waves: &WaveData, item_ref: &DisplayedItemRef) -> Option<f32> {
87 match waves.get_displayed_item_index(item_ref) {
88 Some(vidx) => {
89 let info = waves.drawing_infos.get(vidx.0)?;
90 Some(info.center())
91 }
92 None => None,
93 }
94}
95
96#[derive(Clone, Serialize, Deserialize)]
97pub struct ArrowAnnotation {
98 pub from: WavePoint,
99 pub to: WavePoint,
100 pub created_at: DateTime<Local>,
101 pub length: f32,
102 pub head_mode: ArrowHeadMode,
103 pub annotation_data: AnnotationData,
104}
105
106impl Annotatable for ArrowAnnotation {
107 fn get_id(&self) -> Id {
108 self.annotation_data.id
109 }
110 fn get_type(&self) -> &str {
111 DEFAULT_TYPE
112 }
113 fn set_name(&mut self, name: &str) {
114 self.annotation_data.name = name.to_string();
115 }
116
117 fn get_name(&self) -> String {
118 self.annotation_data.name.clone()
119 }
120
121 fn is_selected(&mut self) {
122 self.annotation_data.stroke.width *= SELECTED_WIDTH_FACTOR;
123 self.annotation_data
124 .stroke
125 .color
126 .gamma_multiply(SELECTED_GAMMA_FACTOR);
127 }
128
129 fn set_visibility(&mut self, visible: bool) {
130 self.annotation_data.visible = visible;
131 }
132
133 fn show_comments(&self) -> bool {
134 self.annotation_data.show_comments
135 }
136
137 fn set_show_comments(&mut self, show: bool) {
138 self.annotation_data.show_comments = show;
139 }
140
141 fn show_comment_box(&self) -> bool {
142 self.annotation_data.comment_box.visible
143 }
144
145 fn is_visible(&self) -> bool {
146 self.annotation_data.visible
147 }
148
149 fn get_center_time(&self) -> BigInt {
150 (&self.from.time + &self.to.time) / 2
151 }
152
153 fn get_start_time(&self) -> BigInt {
154 self.from.time.clone()
155 }
156
157 fn get_end_time(&self) -> BigInt {
158 self.to.time.clone()
159 }
160
161 fn is_attached(&self, removed_ref: &DisplayedItemRef) -> bool {
162 self.to.attached_item.as_ref() == Some(removed_ref)
163 }
164
165 fn get_from_wave(&self) -> Option<GraphicsY> {
166 if let Some(item) = self.from.attached_item {
168 let temp_graphics = GraphicsY {
169 item,
170 anchor: crate::graphics::Anchor::Center,
171 };
172
173 return Some(temp_graphics);
174 }
175
176 None
177 }
178
179 fn get_to_wave(&self) -> Option<GraphicsY> {
180 if let Some(item) = self.to.attached_item {
181 let temp_graphics = GraphicsY {
182 item,
183 anchor: crate::graphics::Anchor::Center,
184 };
185
186 return Some(temp_graphics);
187 }
188
189 None
190 }
191
192 fn draw(
193 &self,
194 ui: &mut Ui,
195 waves: &WaveData,
196 viewport_idx: usize,
197 ctx: &mut DrawingContext,
198 theme: &SurferTheme,
199 msgs: &mut Vec<Message>,
200 _y_offset: f32,
201 to_screen: RectTransform,
202 time_formatter: &TimeFormatter,
203 ) {
204 let mut arrow_annotation = self.clone();
205 arrow_annotation.annotation_data.stroke =
206 Stroke::new(theme.annotation_arrow.width, theme.annotation_arrow.color);
207
208 if waves.selected_annotation == Some(self.annotation_data.id) {
209 arrow_annotation.is_selected();
210 }
211
212 let max_timestamp: BigInt = waves.safe_max_timestamp();
213 let time_offset = waves.time_offset();
214 let viewport = waves.viewports[viewport_idx];
215 let frame_width = ctx.cfg.canvas_size.x;
216
217 arrow_annotation.annotation_data.id =
218 egui::Id::new(("arrow", self.annotation_data.id, viewport_idx));
219
220 let to_y = match self.to.attached_item.as_ref() {
223 Some(item_ref) => match item_center_y(waves, item_ref) {
224 Some(y) => y,
225 None => return,
226 },
227 None => return,
228 };
229
230 let from_y = match self.head_mode {
233 ArrowHeadMode::End => to_y - self.length,
234 ArrowHeadMode::Double => match self.from.attached_item.as_ref() {
235 Some(item_ref) => match item_center_y(waves, item_ref) {
236 Some(y) => y,
237 None => return,
238 },
239 None => return,
240 },
241 };
242
243 let new_to_x = viewport.pixel_from_time(
245 &arrow_annotation.to.time,
246 frame_width,
247 &max_timestamp,
248 time_offset,
249 );
250
251 let new_from_x = viewport.pixel_from_time(
252 &arrow_annotation.from.time,
253 frame_width,
254 &max_timestamp,
255 time_offset,
256 );
257
258 let mut new_to: Pos2 = (ctx.to_screen)(new_to_x, to_y);
259 let mut new_from = (ctx.to_screen)(new_from_x, from_y);
260
261 new_to.y = to_y;
263 new_from.y = from_y;
264
265 arrow_annotation.to.screen_pos = new_to;
266 arrow_annotation.from.screen_pos = new_from;
267
268 let pointer_hover_pos = ui.input(|i| i.pointer.hover_pos());
270 let pointer_click_pos = ui.input(|i| i.pointer.interact_pos());
271 let primary_clicked = ui.input(|i| i.pointer.primary_clicked());
272
273 let exact_hovered = pointer_hover_pos
274 .and_then(|p| arrow_annotation.hit_distance_screen(p))
275 .is_some();
276
277 let exact_clicked = primary_clicked
278 && pointer_click_pos
279 .and_then(|p| arrow_annotation.hit_distance_screen(p))
280 .is_some();
281
282 ui.add(arrow_annotation);
283
284 if exact_clicked {
285 msgs.push(Message::SetActiveViewport(viewport_idx));
289 msgs.push(Message::AnnotationClicked(
290 Some(self.annotation_data.id),
291 pointer_click_pos,
292 Some(viewport_idx),
293 Some(to_screen),
294 Some(ctx.cfg.canvas_size.x),
295 ));
296 msgs.push(Message::ClickHandled());
297 }
298
299 if exact_hovered && let Some(pointer_pos) = pointer_hover_pos {
300 let hover_rect = egui::Rect::from_center_size(pointer_pos, egui::vec2(1.0, 1.0));
303
304 let hover_response = ui.interact(
305 hover_rect,
306 egui::Id::new(("arrow_hover_info", self.annotation_data.id, viewport_idx)),
307 egui::Sense::hover(),
308 );
309
310 let hover_start_time = time_formatter.format(&self.from.time.clone());
311 let hover_end_time = time_formatter.format(&self.to.time.clone());
312
313 let group_name = waves
314 .annotation_groups
315 .iter()
316 .find(|group| group.annotations.contains(&self.get_id()))
317 .map_or(DEFAULT_GROUP_NAME, |group| &group.name);
318 hover_response.on_hover_ui(|ui| {
319 self.draw_hover_info(group_name, ui, (&hover_start_time, &hover_end_time));
320 });
321 }
322 }
323
324 fn get_comment_position(
325 &self,
326 viewport: &Viewport,
327 ctx: &DrawingContext,
328 waves: &WaveData,
329 _offset: f32,
330 ) -> Pos2 {
331 let max_timestamp = waves.safe_max_timestamp();
332 let time_offset = waves.time_offset();
333 let mut x;
334 let mut y = match self.to.attached_item.as_ref() {
335 Some(item_ref) => item_center_y(waves, item_ref).unwrap_or(0.),
336 None => 0.,
337 };
338 match self.head_mode {
339 ArrowHeadMode::End => {
340 x = viewport.pixel_from_time(
341 &self.to.time,
342 ctx.cfg.canvas_size.x,
343 &max_timestamp,
344 time_offset,
345 );
346 }
347 ArrowHeadMode::Double => {
348 x = viewport.pixel_from_time(
350 &self.from.time,
351 ctx.cfg.canvas_size.x,
352 &max_timestamp,
353 time_offset,
354 );
355 let from_y = match self.from.attached_item.as_ref() {
356 Some(item_ref) => item_center_y(waves, item_ref).unwrap_or(0.),
357 None => 0.,
358 };
359 y = f32::midpoint(y, from_y);
360 let to_x = viewport.pixel_from_time(
361 &self.to.time,
362 ctx.cfg.canvas_size.x,
363 &max_timestamp,
364 time_offset,
365 );
366 x = f32::midpoint(x, to_x);
367 }
368 }
369 x = (ctx.to_screen)(x, 0.).x;
370 Pos2::new(x, y)
371 }
372
373 fn get_time_info(&self, time_formatter: &TimeFormatter) -> String {
374 match self.head_mode {
375 ArrowHeadMode::End => format!(
376 "Pointing at {}",
377 time_formatter.format(&self.to.time.clone())
378 ),
379 ArrowHeadMode::Double => format!(
380 "from: {}, to: {}",
381 time_formatter.format(&self.from.time.clone()),
382 time_formatter.format(&self.to.time.clone())
383 ),
384 }
385 }
386
387 fn get_comment_box(&self) -> Comment {
388 self.annotation_data.comment_box.clone()
389 }
390
391 fn get_comment_box_mut(&mut self) -> &mut Comment {
392 &mut self.annotation_data.comment_box
393 }
394
395 fn get_messages(&self) -> Vec<crate::comment::CommentMessage> {
396 self.annotation_data.comment_box.message_chain.clone()
397 }
398}
399
400impl ArrowAnnotation {
401 pub(crate) fn new(
402 id: Id,
403 from: WavePoint,
404 to: WavePoint,
405 head_mode: ArrowHeadMode,
406 num: i32,
407 ) -> Self {
408 let name = format!("{DEFAULT_TYPE} {num}");
409 let annotation_data = AnnotationData::new(id, name, num);
410
411 ArrowAnnotation {
412 from: from.clone(),
413 to: to.clone(),
414 created_at: Local::now(),
415 length: to.screen_pos.y - from.screen_pos.y,
416 head_mode,
417 annotation_data,
418 }
419 }
420
421 #[must_use]
422 pub fn created_at_string(&self) -> String {
423 self.created_at.format("%Y-%m-%d %H:%M").to_string()
424 }
425 pub fn toggle_arrow_visibility(&mut self) {
426 self.annotation_data.visible = !self.annotation_data.visible;
427 }
428
429 fn hit_radius(&self) -> f32 {
430 self.annotation_data.stroke.width + HITBOX_SIZE
431 }
432
433 fn segments(&self) -> Option<ArrowSegments> {
435 let end_head = arrow_geometry(
436 self.from.screen_pos,
437 self.to.screen_pos,
438 self.annotation_data.stroke.width,
439 )?;
440 let (end_base, end_left, end_right) = end_head;
441
442 let start_head: Option<(Pos2, Pos2, Pos2)> = match self.head_mode {
443 ArrowHeadMode::End => None,
444 ArrowHeadMode::Double => arrow_geometry(
445 self.to.screen_pos,
446 self.from.screen_pos,
447 self.annotation_data.stroke.width,
448 ),
449 };
450
451 let shaft_start = match start_head {
452 Some((start_base, _, _)) => start_base,
453 None => self.from.screen_pos,
454 };
455
456 let shaft_end = end_base;
457
458 let (start_tip, start_left, start_right) = match start_head {
459 Some((_base, left, right)) => (Some(self.from.screen_pos), Some(left), Some(right)),
460 None => (None, None, None),
461 };
462
463 Some(ArrowSegments {
464 shaft_start,
465 shaft_end,
466 end_tip: self.to.screen_pos,
467 end_left,
468 end_right,
469 start_tip,
470 start_left,
471 start_right,
472 })
473 }
474 #[must_use]
476 pub fn hit_distance_screen(&self, pointer: Pos2) -> Option<f32> {
477 if self.is_visible() {
478 let seg = self.segments()?;
479 let hit_radius = self.hit_radius();
480
481 let mut best = f32::INFINITY;
482
483 best = best.min(distance_to_segment(pointer, seg.shaft_start, seg.shaft_end));
485
486 best = best.min(distance_to_segment(pointer, seg.end_tip, seg.end_left));
488 best = best.min(distance_to_segment(pointer, seg.end_tip, seg.end_right));
489 best = best.min(distance_to_segment(pointer, seg.end_left, seg.end_right));
490
491 if let (Some(start_tip), Some(start_left), Some(start_right)) =
493 (seg.start_tip, seg.start_left, seg.start_right)
494 {
495 best = best.min(distance_to_segment(pointer, start_tip, start_left));
496 best = best.min(distance_to_segment(pointer, start_tip, start_right));
497 best = best.min(distance_to_segment(pointer, start_left, start_right));
498 }
499
500 if best <= hit_radius { Some(best) } else { None }
501 } else {
502 let radius = (self.annotation_data.stroke.width * 2.0) + HITBOX_SIZE;
503 let mut best = (pointer - self.to.screen_pos).length();
504
505 if let ArrowHeadMode::Double = self.head_mode {
506 best = best.min((pointer - self.from.screen_pos).length());
507 }
508
509 if best <= radius { Some(best) } else { None }
510 }
511 }
512
513 fn paint_arrow_head(&self, ui: &mut Ui, tip: Pos2, left: Pos2, right: Pos2) {
514 ui.painter()
515 .line_segment([tip, left], self.annotation_data.stroke);
516 ui.painter()
517 .line_segment([tip, right], self.annotation_data.stroke);
518 ui.painter()
519 .line_segment([left, right], self.annotation_data.stroke);
520 }
521
522 #[must_use]
524 pub fn get_pos(
525 &self,
526 waves: &WaveData,
527 viewport: &Viewport,
528 ctx: &DrawingContext,
529 offset_y: f32,
530 ) -> Option<Pos2> {
531 let max_timestamp = waves.safe_max_timestamp();
532 let time_offset = waves.time_offset();
533
534 let to_x = viewport.pixel_from_time(
535 &self.to.time,
536 ctx.cfg.canvas_size.x,
537 &max_timestamp,
538 time_offset,
539 );
540 let to_y = self.to.screen_pos.y;
541 let mut position = (ctx.to_screen)(to_x, to_y);
542 position.y = to_y + offset_y;
543
544 Some(position)
545 }
546}
547
548impl Widget for ArrowAnnotation {
549 fn ui(self, ui: &mut Ui) -> Response {
550 let _response = ui.allocate_response(egui::Vec2::ZERO, egui::Sense::empty());
553 if !self.is_visible() {
554 self.hide_annotation(ui, self.annotation_data.stroke, self.to.screen_pos);
555
556 if let ArrowHeadMode::Double = self.head_mode {
557 self.hide_annotation(ui, self.annotation_data.stroke, self.from.screen_pos);
558 }
559 } else if let Some(seg) = self.segments() {
560 ui.painter().line_segment(
562 [seg.shaft_start, seg.shaft_end],
563 self.annotation_data.stroke,
564 );
565
566 self.paint_arrow_head(ui, seg.end_tip, seg.end_left, seg.end_right);
568
569 if let (Some(start_tip), Some(start_left), Some(start_right)) =
571 (seg.start_tip, seg.start_left, seg.start_right)
572 {
573 self.paint_arrow_head(ui, start_tip, start_left, start_right);
574 }
575 }
576 _response
577 }
578}
579
580impl WaveData {
581 #[must_use]
583 pub fn item_ref_at_canvas_y(&self, y: f32) -> Option<DisplayedItemRef> {
584 let vidx = self.get_item_at_y(y)?;
585 let node = self.items_tree.get_visible(vidx)?;
586 Some(node.item_ref)
587 }
588}