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