Skip to main content

blockworx_tools/
edit_route.rs

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        /// Where the route was last picked/edited, in world space. The selection
33        /// overlay anchors here so it opens next to the click rather than at the
34        /// route's bounding-box corner.
35        anchor: Pos2,
36    },
37    /// While dragging an edge, `start_cursor`/`end_cursor` accumulate the raw
38    /// (sub-grid) drag deltas. The working corner positions are snapped each
39    /// frame from these cursors, so small per-frame deltas don't get rounded
40    /// to zero before they accumulate past a grid step.
41    DraggingEdge {
42        id: RouteId,
43        direction: RouteDirection,
44        start_cursor: Pos2,
45        end_cursor: Pos2,
46        /// The edit plan for the two boundary corners, built lazily on the
47        /// first `Dragging` frame — after the select→drag tool switch and its
48        /// re-route, which would otherwise invalidate a plan made earlier.
49        /// `None` until then. While it lives, the document is never written:
50        /// the drag previews into derived state and commits once on release.
51        session: Option<RouteEditSession>,
52    },
53    DraggingWaypoint {
54        id: RouteId,
55        cursor: Pos2,
56        /// The edit plan for the dragged corner, built lazily on the first
57        /// `Dragging` frame (see `DraggingEdge::session`). `None` until then.
58        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                    // Click another object to select it directly; empty canvas
130                    // deselects. (Clicking this route resolves back to itself.)
131                    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                    // Dragging this route's edge/waypoint edits it; dragging
136                    // anything else starts moving that object straight away.
137                    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                    // Release: the one document write of the whole edit — the
160                    // working corners land locked (user-placed) and the final
161                    // geometry is promoted to stored waypoints. Skip if the drag
162                    // never realized (no `Dragging` frame occurred).
163                    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                    // Release: commit the working corner (locked, user-placed) and
181                    // promote the final geometry. Skip if the drag never realized.
182                    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    /// Begin editing route `id` already mid-drag, as if it had been selected and
196    /// the drag started on it. Lets the select/resize tools hand off a drag that
197    /// lands on a route edge, skipping the separate click-to-select step. Returns
198    /// `None` if `pos` isn't on a draggable part of the route.
199    ///
200    /// Borrows the drawing immutably, which is the whole of the "lazy" in
201    /// lazy edge drag: starting a drag *cannot* stage waypoints, because it
202    /// has no way to write — a staged plan would be thrown away by the
203    /// select→drag tool switch's re-route, and this signature makes staging
204    /// one unrepresentable.
205    pub(crate) fn drag_from(data: &Drawing, id: RouteId, pos: Pos2) -> Option<Self> {
206        start_drag(data, id, pos)
207    }
208
209    /// Advance a live drag by this frame's `event` and relay the wire through
210    /// its working corners, into derived state only — no autoroute, so the
211    /// segment goes exactly where the user drags it. The plan is built on the
212    /// first frame that actually drags, after the select→drag tool switch has
213    /// re-routed (which would invalidate a plan staged earlier); an event-less
214    /// repaint re-relays the plan it already has.
215    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/// One drag frame's working state, for the overlay: the plan, the cursors
339/// driving the working corners, and — for a waypoint drag — the grabbed
340/// corner that gets its emphasis ring.
341#[derive(Clone, Copy)]
342struct DragFrame<'a> {
343    id: RouteId,
344    session: &'a RouteEditSession,
345    cursors: &'a [Pos2],
346    grabbed: Option<Pos2>,
347}
348
349/// Draw a drag frame: the edited route in `Editing` mode (halo, no stored
350/// decorations — the document's corner set is stale mid-drag), then the
351/// working state on top: the session's hypothetical handles, the text
352/// anchors on the previewed geometry, each label pinned at its captured
353/// drag-start anchor, and the drag's own emphasis (the projected segment
354/// for an edge drag, the grabbed-corner ring for a waypoint drag). Before
355/// the first `Dragging` frame there is no session and the stored set is
356/// still current, so the route draws as plain `Selected` instead.
357fn 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            // Pin each label at its drag-start world anchor: re-project the
388            // anchor onto the previewed geometry, exactly as the commit's
389            // re-anchoring will.
390            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                // The grabbed corner's ring, over its handle.
397                painter.circle(
398                    px_point(grid_point(cursor)),
399                    PORT_RADIUS,
400                    Role::WaypointFill,
401                    (1.0, Role::ControlHandleFill),
402                );
403            } else {
404                // The dragged segment's projection.
405                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
414/// Classify where `pos` lands on route `id` and return the matching lazy drag
415/// state — reading only, staging no document mutation. The edit plan is built
416/// on the drag's first `Dragging` frame (see [`EditRoute::widget`]), after the
417/// select→drag tool switch has re-routed the wire.
418/// Priority: waypoint → corner → edge center.
419fn 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    // 1. Existing waypoint.
427    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    // 2. Corner (junction of two edges).
438    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    // 3. Edge center — draggable only if two boundary waypoints fit, each at
448    // least a cell from either anchor (a shorter linear segment is not editable).
449    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
467/// Seed positions for an edge drag's two boundary waypoints, or `None` when the
468/// edge can't host them. Every waypoint a drag adds must sit at least one cell
469/// from either route anchor (one on an anchor would overlap the pin), so an edge
470/// end that lands on an anchor — a straight pin-to-pin segment — is pulled one
471/// cell inward along the edge. A segment shorter than three cells leaves no room
472/// for two such waypoints and is not editable. An interior edge's ends are
473/// existing corners, already clear of the anchors, and are used unchanged.
474fn 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; // one cell along the edge
483    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
499/// Whether grid point `p` is one of the route's fixed anchors.
500fn is_anchor(geometry: &RouteGeometry, p: blockworx_doc::geometry::GridPoint) -> bool {
501    p == geometry.start_pos || p == geometry.end_pos
502}
503
504/// Whether `pos` sits at least one cell from both anchors — the invariant every
505/// waypoint added during a drag must satisfy (a closer one would overlap a pin).
506fn 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}