Skip to main content

blockworx/tools/
add_route_label.rs

1use crate::render::RouteRenderMode;
2use crate::theme::Style;
3use blockworx_doc::id::RouteId;
4use blockworx_geom::Pos2;
5use blockworx_paint::{Canvas, Cursor, Event, Interaction};
6
7use crate::{
8    tools::{
9        RenameRoute, SelectTool,
10        names::ToolName,
11        tool::{Action, ToolTrait},
12    },
13    widget::{DrawingPasses, drawing::Drawing},
14};
15
16#[derive(Default)]
17pub enum AddRouteLabel {
18    #[default]
19    Idle,
20    Hovered {
21        route_id: RouteId,
22    },
23    /// Armed from the route selection overlay: the route is already known, so the
24    /// next frame drops a label at its midpoint and opens the editor — no click.
25    Armed(RouteId),
26}
27
28impl ToolTrait for AddRouteLabel {
29    fn name(&self) -> ToolName {
30        ToolName::AddRouteLabel
31    }
32
33    fn widget<C: Canvas>(
34        &mut self,
35        data: &mut Drawing,
36        interaction: &Interaction,
37        painter: &mut Style<'_, C>,
38    ) -> Option<Action> {
39        painter.set_cursor(Cursor::Crosshair);
40        let state = std::mem::take(self);
41        let mut action = None;
42        *self = match state {
43            AddRouteLabel::Idle => match interaction.event {
44                Some(Event::HoverAt(pos)) => {
45                    if let Some(id) = route_hit(data, pos) {
46                        render_all(data, interaction, painter, Some(id));
47                        AddRouteLabel::Hovered { route_id: id }
48                    } else {
49                        render_all(data, interaction, painter, None);
50                        AddRouteLabel::Idle
51                    }
52                }
53                Some(Event::Clicked { pos }) => {
54                    if let Some(id) = route_hit(data, pos) {
55                        action = Some(add_label(data, id, pos, painter));
56                    }
57                    render_all(data, interaction, painter, None);
58                    AddRouteLabel::Idle
59                }
60                _ => {
61                    render_all(data, interaction, painter, None);
62                    AddRouteLabel::Idle
63                }
64            },
65
66            AddRouteLabel::Hovered { route_id } => {
67                render_all(data, interaction, painter, Some(route_id));
68                match interaction.event {
69                    Some(Event::HoverAt(pos)) => match route_hit(data, pos) {
70                        Some(id) => AddRouteLabel::Hovered { route_id: id },
71                        None => AddRouteLabel::Idle,
72                    },
73                    Some(Event::Clicked { pos }) => {
74                        let hit_id = route_hit(data, pos).unwrap_or(route_id);
75                        action = Some(add_label(data, hit_id, pos, painter));
76                        AddRouteLabel::Idle
77                    }
78                    _ => AddRouteLabel::Hovered { route_id },
79                }
80            }
81
82            AddRouteLabel::Armed(route_id) => {
83                if let Some(mid) = data
84                    .route_geometry(route_id)
85                    .and_then(|g| route_midpoint(&g.points()))
86                {
87                    action = Some(add_label(data, route_id, mid, painter));
88                }
89                render_all(data, interaction, painter, None);
90                AddRouteLabel::Idle
91            }
92        };
93        action
94    }
95}
96
97/// The point halfway along `points` by arc length — where an overlay-triggered
98/// label is dropped, since the overlay has no click position.
99fn route_midpoint(points: &[Pos2]) -> Option<Pos2> {
100    let total: f32 = points.windows(2).map(|w| w[0].distance(w[1])).sum();
101    let mut remaining = total / 2.0;
102    for w in points.windows(2) {
103        let seg = w[0].distance(w[1]);
104        if seg >= remaining {
105            let frac = if seg > 0.0 { remaining / seg } else { 0.0 };
106            return Some(w[0] + frac * (w[1] - w[0]));
107        }
108        remaining -= seg;
109    }
110    points.last().copied()
111}
112
113/// Add a name label to `route_id` at `pos` and re-route. If the route had no
114/// label before, open its text editor so the user can name it straight away;
115/// otherwise (a subsequent label on an already-named route, which opens no
116/// editor) return to the select tool rather than staying in add-label mode.
117fn add_label<C: Canvas>(
118    data: &mut Drawing,
119    route_id: RouteId,
120    pos: Pos2,
121    painter: &Style<'_, C>,
122) -> Action {
123    let was_empty = data
124        .auto_route(route_id)
125        .is_some_and(|wire| wire.labels.is_empty());
126    let first_label = data.add_route_label(route_id, pos).filter(|_| was_empty);
127    match first_label {
128        Some(label_id) => RenameRoute::action(data, route_id, label_id, painter),
129        None => Action::SwitchTool(SelectTool.into()),
130    }
131}
132
133fn render_all<C: Canvas>(
134    data: &Drawing,
135    _interaction: &Interaction,
136    painter: &mut Style<'_, C>,
137    highlighted: Option<RouteId>,
138) {
139    DrawingPasses::new(data)
140        .route_mode(move |id| {
141            if Some(id) == highlighted {
142                RouteRenderMode::Highlighted
143            } else {
144                RouteRenderMode::Normal
145            }
146        })
147        .draw(painter);
148}
149
150fn route_hit(data: &Drawing, pos: Pos2) -> Option<RouteId> {
151    data.auto_routes().map(|(id, _)| id).find(|id| {
152        data.route_geometry(*id)
153            .is_some_and(|g| g.hovered_edge(pos).is_some())
154    })
155}