1use crate::render::{
2 RouteRenderMode, render_path_with_chamfered_corners, render_route_label_at,
3 render_text_anchors, render_waypoint_handles, route_text_color,
4};
5use crate::theme::Style;
6use blockworx_doc::id::RouteId;
7use blockworx_geom::{Pos2, vec2};
8use blockworx_paint::{Canvas, Event, Interaction};
9
10use crate::{
11 grid::{GRID_SIZE, HIT_RADIUS, PORT_RADIUS, grid_point, px_point, snap_to_grid},
12 names::ToolName,
13 presentation::{RouteDirection, RouteEdge},
14 theme::Role,
15 tool::{Action, Deletable, PreviewPhase, ToolTrait, Transition},
16 widget::{
17 DrawingPasses,
18 auto_route::{RouteGeometry, hit_waypoint, text_anchors},
19 drawing::Drawing,
20 edge::RouteEdgeExt,
21 routing::RouteEditSession,
22 },
23};
24
25pub enum EditRoute {
26 Idle,
27 Hovered {
28 id: RouteId,
29 },
30 Selected {
31 id: RouteId,
32 anchor: Pos2,
36 },
37 DraggingEdge {
42 id: RouteId,
43 direction: RouteDirection,
44 start_cursor: Pos2,
45 end_cursor: Pos2,
46 session: Option<RouteEditSession>,
52 },
53 DraggingWaypoint {
54 id: RouteId,
55 cursor: Pos2,
56 session: Option<RouteEditSession>,
59 },
60}
61
62impl ToolTrait for EditRoute {
63 fn name(&self) -> ToolName {
64 ToolName::EditRoute
65 }
66
67 fn selection(&self) -> Option<Deletable> {
68 match self {
69 EditRoute::Selected { id, .. } => Some(Deletable::Route(*id)),
70 _ => None,
71 }
72 }
73
74 fn overlay_anchor(&self) -> Option<Pos2> {
75 match self {
76 EditRoute::Selected { anchor, .. } => Some(*anchor),
77 _ => None,
78 }
79 }
80
81 fn preview<C: Canvas>(
82 &mut self,
83 data: &mut Drawing,
84 interaction: &Interaction,
85 _painter: &mut Style<'_, C>,
86 phase: &PreviewPhase,
87 ) {
88 self.preview_drag(data, interaction.event, phase);
89 }
90
91 fn widget<C: Canvas>(
92 &mut self,
93 data: &mut Drawing,
94 interaction: &Interaction,
95 painter: &mut Style<'_, C>,
96 ) -> Option<Transition> {
97 self.render(data, interaction, painter);
98 match self {
99 EditRoute::Idle => {
100 if let Some(Event::HoverAt(pos)) = interaction.event {
101 if let Some(id) = data.route_at_pos(pos) {
102 *self = EditRoute::Hovered { id };
103 }
104 } else if let Some(Event::Clicked { pos }) = interaction.event
105 && let Some(id) = data.route_at_pos(pos)
106 {
107 *self = EditRoute::Selected { id, anchor: pos };
108 }
109 }
110
111 EditRoute::Hovered { id } => {
112 let id = *id;
113 if let Some(Event::HoverAt(pos)) = interaction.event {
114 match data.route_at_pos(pos) {
115 Some(new_id) => *self = EditRoute::Hovered { id: new_id },
116 None => return Some(Transition::default()),
117 }
118 } else if let Some(Event::Clicked { pos }) = interaction.event {
119 *self = EditRoute::Selected { id, anchor: pos };
120 }
121 }
122
123 EditRoute::Selected { id, .. } => {
124 let id = *id;
125 if interaction.delete_pressed {
126 return Some(Action::Delete(Deletable::Route(id)).into());
127 }
128 if let Some(Event::Clicked { pos }) = interaction.event {
129 return Some(
132 crate::select_tool::click_to_select(data, pos, painter).unwrap_or_default(),
133 );
134 } else if let Some(Event::DragStarted { pos }) = interaction.event {
135 match start_drag(data, id, pos) {
138 Some(new_state) => *self = new_state,
139 None => {
140 return Some(crate::select_tool::drag_to_move(
141 data,
142 pos,
143 painter,
144 crate::select_tool::IconGrab::NotArmed,
145 ));
146 }
147 }
148 }
149 }
150
151 EditRoute::DraggingEdge {
152 id,
153 start_cursor,
154 end_cursor,
155 session,
156 ..
157 } => {
158 if let Some(Event::DragStopped { .. }) = interaction.event {
159 let (id, start, end) = (*id, *start_cursor, *end_cursor);
164 if let Some(s) = session {
165 data.commit_route_edit(id, s, &[start, end]);
166 }
167 *self = EditRoute::Selected {
168 id,
169 anchor: start + (end - start) * 0.5,
170 };
171 }
172 }
173
174 EditRoute::DraggingWaypoint {
175 id,
176 cursor,
177 session,
178 } => {
179 if let Some(Event::DragStopped { .. }) = interaction.event {
180 let (id, cursor) = (*id, *cursor);
183 if let Some(s) = session {
184 data.commit_route_edit(id, s, &[cursor]);
185 }
186 *self = EditRoute::Selected { id, anchor: cursor };
187 }
188 }
189 }
190 None
191 }
192}
193
194impl EditRoute {
195 pub(crate) fn drag_from(data: &Drawing, id: RouteId, pos: Pos2) -> Option<Self> {
206 start_drag(data, id, pos)
207 }
208
209 fn preview_drag(&mut self, data: &mut Drawing<'_>, event: Option<Event>, phase: &PreviewPhase) {
216 let dragged = match event {
217 Some(Event::Dragging { delta, .. }) => Some(delta),
218 _ => None,
219 };
220 match self {
221 EditRoute::DraggingEdge {
222 id,
223 direction,
224 start_cursor,
225 end_cursor,
226 session,
227 } => {
228 if dragged.is_some() && session.is_none() {
229 *session = data.route_edit_session(*id, &[*start_cursor, *end_cursor]);
230 }
231 let Some(s) = session else { return };
232 if let Some(delta) = dragged {
233 let constrained = match direction {
234 RouteDirection::Horizontal => vec2(0.0, delta.y),
235 RouteDirection::Vertical => vec2(delta.x, 0.0),
236 };
237 *start_cursor += constrained;
238 *end_cursor += constrained;
239 }
240 data.preview_route_edit(phase, *id, s, &[*start_cursor, *end_cursor]);
241 }
242 EditRoute::DraggingWaypoint {
243 id,
244 cursor,
245 session,
246 } => {
247 if dragged.is_some() && session.is_none() {
248 *session = data.route_edit_session(*id, &[*cursor]);
249 }
250 let Some(s) = session else { return };
251 if let Some(delta) = dragged {
252 *cursor += delta;
253 }
254 data.preview_route_edit(phase, *id, s, &[*cursor]);
255 }
256 _ => {}
257 }
258 }
259
260 fn render<C: Canvas>(
261 &self,
262 data: &Drawing,
263 interaction: &Interaction,
264 painter: &mut Style<'_, C>,
265 ) {
266 match self {
267 EditRoute::Idle => {
268 crate::widget::display::widget(data, interaction, painter);
269 }
270 EditRoute::Hovered { id } => {
271 let id = *id;
272 DrawingPasses::new(data)
273 .route_mode(move |rid| {
274 if rid == id {
275 RouteRenderMode::Highlighted
276 } else {
277 RouteRenderMode::Normal
278 }
279 })
280 .draw(painter);
281 }
282 EditRoute::Selected { id, .. } => {
283 render_selected(data, painter, *id);
284 }
285 EditRoute::DraggingEdge {
286 id,
287 start_cursor,
288 end_cursor,
289 session,
290 ..
291 } => match session {
292 Some(session) => render_dragging(
293 data,
294 painter,
295 DragFrame {
296 id: *id,
297 session,
298 cursors: &[*start_cursor, *end_cursor],
299 grabbed: None,
300 },
301 ),
302 None => render_selected(data, painter, *id),
303 },
304 EditRoute::DraggingWaypoint {
305 id,
306 cursor,
307 session,
308 } => match session {
309 Some(session) => render_dragging(
310 data,
311 painter,
312 DragFrame {
313 id: *id,
314 session,
315 cursors: &[*cursor],
316 grabbed: Some(*cursor),
317 },
318 ),
319 None => render_selected(data, painter, *id),
320 },
321 }
322 }
323}
324
325fn render_selected<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>, id: RouteId) {
326 let authoring = data.authoring();
327 DrawingPasses::new(data)
328 .route_mode(move |rid| {
329 if rid == id {
330 RouteRenderMode::Selected { authoring }
331 } else {
332 RouteRenderMode::Normal
333 }
334 })
335 .draw(painter);
336}
337
338#[derive(Clone, Copy)]
342struct DragFrame<'a> {
343 id: RouteId,
344 session: &'a RouteEditSession,
345 cursors: &'a [Pos2],
346 grabbed: Option<Pos2>,
347}
348
349fn render_dragging<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>, frame: DragFrame) {
358 let DragFrame {
359 id,
360 session,
361 cursors,
362 grabbed,
363 } = frame;
364 let cursors = cursors.to_vec();
365 DrawingPasses::new(data)
366 .route_mode(move |rid| {
367 if rid == id {
368 RouteRenderMode::Editing
369 } else {
370 RouteRenderMode::Normal
371 }
372 })
373 .suppress_route_labels(id)
374 .overlay(move |painter| {
375 let (Some(wire), Some(geometry)) = (data.auto_route(id), data.route_geometry(id))
376 else {
377 return;
378 };
379 render_waypoint_handles(
380 painter,
381 session
382 .corners(&wire.route.waypoints, &cursors)
383 .into_iter()
384 .map(|(pos, lock)| (px_point(pos), lock)),
385 );
386 render_text_anchors(painter, text_anchors(&wire.labels, geometry));
387 let color = route_text_color(wire.route, RouteRenderMode::Editing);
391 for &(_, anchor) in session.anchors() {
392 let at = geometry.map_linear_distance_to_position(geometry.distance_along(anchor));
393 render_route_label_at(painter, wire.route, at, color);
394 }
395 if let Some(cursor) = grabbed {
396 painter.circle(
398 px_point(grid_point(cursor)),
399 PORT_RADIUS,
400 Role::WaypointFill,
401 (1.0, Role::ControlHandleFill),
402 );
403 } else {
404 let projected: Vec<Pos2> =
406 geometry.points().into_iter().map(snap_to_grid).collect();
407 let path = render_path_with_chamfered_corners(&projected);
408 path.render(painter, (1.5, Role::EdgeDragPreview));
409 }
410 })
411 .draw(painter);
412}
413
414fn start_drag(data: &Drawing, id: RouteId, pos: Pos2) -> Option<EditRoute> {
420 if data.authoring().is_withheld() {
421 return None;
422 }
423 let wire = data.auto_route(id)?;
424 let geometry = data.route_geometry(id)?;
425
426 let waypoints = wire.route.waypoints.clone();
428 if let Some(ordinal) = hit_waypoint(&waypoints, pos, HIT_RADIUS.get()) {
429 let wp_pos = waypoints.get(usize::from(ordinal))?.pos;
430 return Some(EditRoute::DraggingWaypoint {
431 id,
432 cursor: px_point(wp_pos),
433 session: None,
434 });
435 }
436
437 if let Some((edge_1, _)) = geometry.hovered_corner(pos) {
439 let corner_pos: Pos2 = geometry.edge(edge_1).map(|e| px_point(e.end))?;
440 return Some(EditRoute::DraggingWaypoint {
441 id,
442 cursor: corner_pos,
443 session: None,
444 });
445 }
446
447 if let Some(edge_id) = geometry.hovered_edge(pos) {
450 let edge = geometry.edge(edge_id)?.clone();
451 if edge.distance(pos).1 <= HIT_RADIUS.get()
452 && let Some((start_cursor, end_cursor)) = boundary_seeds(geometry, &edge)
453 {
454 return Some(EditRoute::DraggingEdge {
455 id,
456 direction: edge.direction(),
457 start_cursor,
458 end_cursor,
459 session: None,
460 });
461 }
462 }
463
464 None
465}
466
467fn boundary_seeds(geometry: &RouteGeometry, edge: &RouteEdge) -> Option<(Pos2, Pos2)> {
475 let start: Pos2 = px_point(edge.start);
476 let end: Pos2 = px_point(edge.end);
477 let along = end - start;
478 let len = along.length();
479 if len == 0.0 {
480 return None;
481 }
482 let step = along / len * GRID_SIZE; let start_seed = if is_anchor(geometry, edge.start) {
484 start + step
485 } else {
486 start
487 };
488 let end_seed = if is_anchor(geometry, edge.end) {
489 end - step
490 } else {
491 end
492 };
493 (clear_of_anchors(geometry, start_seed)
494 && clear_of_anchors(geometry, end_seed)
495 && (end_seed - start_seed).length() >= GRID_SIZE - 0.5)
496 .then_some((start_seed, end_seed))
497}
498
499fn is_anchor(geometry: &RouteGeometry, p: blockworx_doc::geometry::GridPoint) -> bool {
501 p == geometry.start_pos || p == geometry.end_pos
502}
503
504fn clear_of_anchors(geometry: &RouteGeometry, pos: Pos2) -> bool {
507 let cell = GRID_SIZE - 0.5;
508 geometry.start_pos().distance(pos) >= cell && geometry.end_pos().distance(pos) >= cell
509}