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