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 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(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
97fn 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
113fn 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}