Skip to main content

libsurfer/
arrow.rs

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,    // one-headed arrow, with the head at the target/end point.
27    Double, // Double-headed arrow, with heads at both the start and end points.
28}
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
49// Returns the shortest distance between point `p` and the line segment `a -> b`.
50fn 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
64// Calculates the base, left, and right points of an arrow head ending at `to`.
65fn 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}
85/// Returns the vertical center of a displayed waveform item in global coordinates.
86fn 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        //this REALLY should be changed, arrow should likely just use a GraphicsY instead of WavePoint
167        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        // `item_center_y` returns a global y-coordinate, so it does not need to be
221        // converted through `ctx.to_screen`.
222        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        // A one-headed arrow keeps its original vertical length. A double-headed arrow
231        // follows the vertical centers of both attached items.
232        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        // Convert annotation times into viewport-local x pixel positions.
244        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        //Preserve global y-coordinates because waveform rows already use global canvas y.
262        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        // Get hover/click position for hit detection
269        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            // Notify the application that this annotation was clicked and that the
286            // current viewport should become active
287
288            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            // Use a tiny hover rectangle at the pointer position to attach egui's
301            // tooltip UI to the actual arrow hit location.
302            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                // For double-headed arrows, place comments near the visual midpoint.
349                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    // Builds all drawable and hit-testable arrow segments from the current screen positions.
434    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    /// Returns the pointer distance to the arrow if it is inside the hit radius.
475    #[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            // Compare to the shaft
484            best = best.min(distance_to_segment(pointer, seg.shaft_start, seg.shaft_end));
485
486            // Compare to the end point 3 segment
487            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            // Compare to the arrow head at start, if it is dubbelheaded arrow.
492            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    /// Returns arrow `end_position` in global coordinates
523    #[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        // The widget does custom painting and uses explicit hit detection elsewhere,
551        // so it only allocates an empty egui response here.
552        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            // Paint shaft
561            ui.painter().line_segment(
562                [seg.shaft_start, seg.shaft_end],
563                self.annotation_data.stroke,
564            );
565
566            // Paint arrow head at the end of the arrow
567            self.paint_arrow_head(ui, seg.end_tip, seg.end_left, seg.end_right);
568
569            // Paint arrow head at the start if it is a doubleheaded arrow.
570            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    /// Returns the displayed item reference located at the given canvas y-coordinate.
582    #[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}