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