blockworx_tools/
add_route_label.rs1use 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(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
95fn 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
111fn 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}