Skip to main content

blockworx_tools/
route_tool.rs

1use std::collections::BTreeMap;
2
3use blockworx_doc::geometry::Waypoint;
4use blockworx_doc::id::PinId;
5use blockworx_geom::Pos2;
6use blockworx_router::{ClosedRouter, point::Point};
7
8use crate::render::render_path_with_chamfered_corners;
9use crate::theme::Style;
10use crate::{
11    SelectTool,
12    edit::create::PathOrdinal,
13    grid::{PORT_RADIUS, grid_point, px_point, snap_to_grid},
14    names::ToolName,
15    new_pin::{SlotMarker, draw_route_markers, marker_at, markers_in_scope, nearest_block_markers},
16    route_start,
17    theme::Role,
18    tool::{ToolTrait, Transition},
19    widget::{
20        drawing::Drawing,
21        waypoint_router::{
22            FixedLegs, RouteRequest, SelfCost, TaggedPoint, route_fixed_legs, route_to_head,
23        },
24    },
25};
26use blockworx_paint::{Canvas, Cursor, Event, Interaction};
27/// The in-progress corners as the router speaks them: waypoints are
28/// positional, so a corner's identity is its index in the list.
29fn waypoint_nodes(waypoints: &[Waypoint]) -> (Vec<PathOrdinal>, BTreeMap<PathOrdinal, Point>) {
30    let positions: BTreeMap<PathOrdinal, Point> = waypoints
31        .iter()
32        .enumerate()
33        .map(|(index, wp)| (PathOrdinal::new(index), waypoint_point(wp)))
34        .collect();
35    (positions.keys().copied().collect(), positions)
36}
37
38fn waypoint_point(wp: &Waypoint) -> Point {
39    Point::from(wp.pos)
40}
41
42/// Route an in-progress route (`start` → each waypoint → `end`) against a freshly
43/// built closed router, seeding the endpoints and waypoints so they are graph
44/// nodes. Used at commit time, where the exact final geometry (and the route's own
45/// occupancy) matters. The live preview instead routes against a cached router
46/// (see `RouteTool::preview_route`).
47fn route_in_progress(
48    data: &Drawing,
49    start: Pos2,
50    waypoints: &[Waypoint],
51    end: Pos2,
52) -> Vec<TaggedPoint> {
53    let mut extra: Vec<Point> = Vec::with_capacity(waypoints.len() + 2);
54    extra.push(start.into());
55    extra.push(end.into());
56    extra.extend(waypoints.iter().map(waypoint_point));
57    let mut router = data.scratch_closed_router(&extra);
58    let (wp_ids, wp_positions) = waypoint_nodes(waypoints);
59    RouteRequest {
60        start: start.into(),
61        end: end.into(),
62        wp_ids: &wp_ids,
63        wp_positions: &wp_positions,
64        self_cost: SelfCost::Apply,
65    }
66    .route(&mut router)
67    .points
68}
69
70#[derive(Default)]
71enum RouteToolState {
72    #[default]
73    Idle,
74    PinHeadHovered {
75        anchor: PinId,
76    },
77    InProgress {
78        start: PinId,
79        waypoints: Vec<Waypoint>,
80        head: Pos2,
81    },
82    Proposed {
83        start: PinId,
84        waypoints: Vec<Waypoint>,
85        finish: FinishTarget,
86    },
87}
88
89/// Where an in-progress route proposes to end. An existing pin resolves to its
90/// anchor directly; a free slot's marker names the block and slot where the pin
91/// will be reconstructed at commit time, along with its prospective connection
92/// point.
93#[derive(Clone, Copy)]
94enum FinishTarget {
95    Anchor(PinId),
96    NewPin(SlotMarker),
97}
98
99/// The in-progress route a preview frame lays out: where it starts, the
100/// waypoints committed so far, and the cursor it currently reaches to.
101struct PreviewRequest<'a> {
102    start: PinId,
103    start_pos: Pos2,
104    waypoints: &'a [Waypoint],
105    end: Pos2,
106}
107
108/// Everything cached across frames of one route gesture while only the cursor
109/// moves. The graph (obstacles + existing routes + `start` + waypoints) and the
110/// fixed `start → … → last waypoint` legs are both invariant until a waypoint is
111/// added, so both are computed once. Keyed by `start` + waypoint count.
112struct PreviewCache {
113    start: PinId,
114    waypoint_count: usize,
115    router: ClosedRouter,
116    fixed: FixedLegs,
117}
118
119#[derive(Default)]
120pub struct RouteTool {
121    state: RouteToolState,
122    preview_path: Vec<TaggedPoint>,
123    /// Cached router + fixed legs for the live preview; rebuilt only when a
124    /// waypoint is added or a new route starts, and dropped when the gesture ends
125    /// (which also covers commit changing the set of existing routes). Boxed to
126    /// keep it off the `Tool` enum's stack footprint — it is the widest thing a
127    /// tool owns, and only the route tool ever has one.
128    preview_cache: Option<Box<PreviewCache>>,
129    /// When set, completing or cancelling the route returns to the select tool
130    /// (the route was started from a select-tool hover target) rather than
131    /// staying in the route tool ready for the next route.
132    return_to_select: bool,
133}
134
135impl ToolTrait for RouteTool {
136    fn name(&self) -> ToolName {
137        ToolName::Route
138    }
139
140    fn widget<C: Canvas>(
141        &mut self,
142        data: &mut Drawing,
143        interaction: &Interaction,
144        painter: &mut Style<'_, C>,
145    ) -> Option<Transition> {
146        // Take ownership of the current state, leaving Idle in place.
147        // This avoids borrow-checker issues when transitioning states.
148        let state = std::mem::take(&mut self.state);
149
150        crate::widget::display::widget(data, interaction, painter);
151        painter.set_cursor(Cursor::Crosshair);
152
153        let was_routing = matches!(
154            state,
155            RouteToolState::InProgress { .. } | RouteToolState::Proposed { .. }
156        );
157        // Esc or Backspace cancels an in-progress route, discarding it.
158        let cancelled = was_routing && (interaction.escape_pressed || interaction.delete_pressed);
159
160        self.state = if cancelled {
161            self.preview_path.clear();
162            RouteToolState::Idle
163        } else {
164            match state {
165                RouteToolState::Idle => match interaction.event {
166                    Some(Event::DragStarted { pos } | Event::Clicked { pos }) => {
167                        if let Some(anchor) = data.anchor_at_pos(pos) {
168                            RouteToolState::InProgress {
169                                start: anchor,
170                                waypoints: Vec::new(),
171                                head: pos,
172                            }
173                        } else {
174                            RouteToolState::Idle
175                        }
176                    }
177                    Some(Event::HoverAt(pos)) => {
178                        if let Some(anchor) = data.anchor_at_pos(pos) {
179                            RouteToolState::PinHeadHovered { anchor }
180                        } else {
181                            RouteToolState::Idle
182                        }
183                    }
184                    _ => RouteToolState::Idle,
185                },
186
187                RouteToolState::PinHeadHovered { anchor } => match interaction.event {
188                    Some(Event::DragStarted { pos } | Event::Clicked { pos }) => {
189                        RouteToolState::InProgress {
190                            start: anchor,
191                            waypoints: Vec::new(),
192                            head: pos,
193                        }
194                    }
195                    Some(Event::HoverAt(pos)) => {
196                        if let Some(new_anchor) = data.anchor_at_pos(pos) {
197                            RouteToolState::PinHeadHovered { anchor: new_anchor }
198                        } else {
199                            RouteToolState::Idle
200                        }
201                    }
202                    _ => RouteToolState::PinHeadHovered { anchor },
203                },
204
205                RouteToolState::InProgress {
206                    start,
207                    mut waypoints,
208                    mut head,
209                } => match interaction.event {
210                    Some(Event::Clicked { pos }) => {
211                        waypoints.push(Waypoint {
212                            pos: grid_point(pos),
213                            locked: true,
214                        });
215                        RouteToolState::InProgress {
216                            start,
217                            waypoints,
218                            head,
219                        }
220                    }
221                    // A drag away from the start anchor previews the route to the
222                    // cursor, proposing a connection once it reaches another anchor or
223                    // an armed new-pin target.
224                    Some(Event::HoverAt(pos) | Event::Dragging { pos, .. }) => {
225                        if let Some(finish) = Self::finish_at(data, start, pos) {
226                            RouteToolState::Proposed {
227                                start,
228                                waypoints,
229                                finish,
230                            }
231                        } else {
232                            head = pos;
233                            RouteToolState::InProgress {
234                                start,
235                                waypoints,
236                                head,
237                            }
238                        }
239                    }
240                    // Releasing on a second anchor (or armed new-pin target) completes
241                    // the route; releasing anywhere else leaves the operation in
242                    // progress, exactly as a click on the start anchor would.
243                    Some(Event::DragStopped { pos }) => {
244                        if let Some(finish) = Self::finish_at(data, start, pos) {
245                            self.commit(data, start, finish, waypoints)
246                        } else {
247                            head = pos;
248                            RouteToolState::InProgress {
249                                start,
250                                waypoints,
251                                head,
252                            }
253                        }
254                    }
255                    _ => RouteToolState::InProgress {
256                        start,
257                        waypoints,
258                        head,
259                    },
260                },
261
262                RouteToolState::Proposed {
263                    start,
264                    waypoints,
265                    mut finish,
266                } => match interaction.event {
267                    Some(Event::Clicked { .. }) => self.commit(data, start, finish, waypoints),
268                    // Releasing on the proposed target completes the route; releasing
269                    // off it continues the operation from the start anchor.
270                    Some(Event::DragStopped { pos }) => match Self::finish_at(data, start, pos) {
271                        Some(finish) => self.commit(data, start, finish, waypoints),
272                        None => RouteToolState::InProgress {
273                            start,
274                            waypoints,
275                            head: pos,
276                        },
277                    },
278                    Some(Event::HoverAt(pos) | Event::Dragging { pos, .. }) => {
279                        match Self::finish_at(data, start, pos) {
280                            Some(new_finish) => {
281                                finish = new_finish;
282                                RouteToolState::Proposed {
283                                    start,
284                                    waypoints,
285                                    finish,
286                                }
287                            }
288                            None => RouteToolState::InProgress {
289                                start,
290                                waypoints,
291                                head: pos,
292                            },
293                        }
294                    }
295                    _ => RouteToolState::Proposed {
296                        start,
297                        waypoints,
298                        finish,
299                    },
300                },
301            }
302        };
303
304        self.update_preview(data);
305        self.render(data, painter);
306        // A committed route is inserted after this frame's canvas was already
307        // drawn (display runs at the top of `widget`), so without a nudge nothing
308        // repaints to show it until the next stray input. Request one frame so the
309        // new route — and any pin it created — appears immediately.
310        let committed = was_routing && !cancelled && matches!(self.state, RouteToolState::Idle);
311        if committed {
312            painter.request_repaint();
313        }
314        // A route started from the select tool returns there once it lands in
315        // Idle — whether by completing (commit) or cancelling.
316        if self.return_to_select && was_routing && matches!(self.state, RouteToolState::Idle) {
317            return Some(Transition::SwitchTool(SelectTool.into()));
318        }
319        None
320    }
321}
322
323impl RouteTool {
324    /// Start routing immediately from `start`, with the route head at `head`:
325    /// the caller hands off mid-gesture and the in-progress handling carries
326    /// the same gesture to a connecting anchor.
327    pub fn routing_from(start: PinId, head: Pos2) -> Self {
328        Self {
329            state: RouteToolState::InProgress {
330                start,
331                waypoints: Vec::new(),
332                head,
333            },
334            preview_path: Vec::new(),
335            preview_cache: None,
336            return_to_select: false,
337        }
338    }
339
340    /// Like [`Self::routing_from`], but completing or cancelling the route returns
341    /// to the select tool. Used when a route is started from a select-tool hover
342    /// target.
343    pub fn routing_from_select(start: PinId, head: Pos2) -> Self {
344        Self {
345            return_to_select: true,
346            ..Self::routing_from(start, head)
347        }
348    }
349
350    /// Build and add the route from `start` to `finish` through `waypoints` and
351    /// return to Idle. A [`FinishTarget::NewPin`] finish reconstructs its pin
352    /// first, in this same frame, so the pin and route collapse into one undo
353    /// step (state is snapshotted once per frame). A degenerate (zero-length)
354    /// route is rejected, leaving the proposal in place — and, importantly, no
355    /// pin is created in that case.
356    fn commit(
357        &mut self,
358        data: &mut Drawing,
359        start: PinId,
360        finish: FinishTarget,
361        waypoints: Vec<Waypoint>,
362    ) -> RouteToolState {
363        let Some(start_pos) = data.anchor(start) else {
364            return RouteToolState::Proposed {
365                start,
366                waypoints,
367                finish,
368            };
369        };
370        let Some(end_pos) = Self::finish_pos(data, &finish) else {
371            return RouteToolState::Proposed {
372                start,
373                waypoints,
374                finish,
375            };
376        };
377        if start_pos == end_pos {
378            return RouteToolState::Proposed {
379                start,
380                waypoints,
381                finish,
382            };
383        }
384        // Only now resolve the finish — a destination the wire drew onto a free
385        // slot stamps its pin in the same commit as the wire, so a rejected
386        // (degenerate) route never leaves an orphan pin.
387        let destination = match finish {
388            FinishTarget::Anchor(anchor) => crate::edit::create::RouteEnd::Pin(anchor),
389            FinishTarget::NewPin(SlotMarker { block, loc, .. }) => {
390                // A stale target can still name a block that has since been
391                // locked, and stamping a pin on one is a material edit — so
392                // the scope has to hand out the proof before the wire can
393                // carry a fresh pin at all.
394                let Some(owner) = data.unlocked_scope(block) else {
395                    return RouteToolState::Proposed {
396                        start,
397                        waypoints,
398                        finish,
399                    };
400                };
401                crate::edit::create::RouteEnd::Fresh(crate::edit::create::NewPin {
402                    id: data.mint(),
403                    owner,
404                    slot: blockworx_doc::geometry::PinSlot {
405                        side: loc.side,
406                        offset: crate::grid::pin_slot(loc.offset),
407                    },
408                })
409            }
410        };
411        let path = route_in_progress(
412            data,
413            snap_to_grid(start_pos),
414            &waypoints,
415            snap_to_grid(end_pos),
416        );
417        // The wire is stored as its corner list, so the solved polyline is
418        // promoted before it lands — the "every corner is a waypoint"
419        // invariant holds from the moment the route exists.
420        let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
421        let corners =
422            crate::widget::reconstruct::promote_corners_to_waypoints(&waypoints, &mut geometry);
423        data.add_route(start, destination, corners);
424        self.preview_path.clear();
425        RouteToolState::Idle
426    }
427
428    /// Resolve a finish target's world position. An existing anchor resolves
429    /// through `data`; a new-pin target reports the slot's prospective connection
430    /// point directly (the pin doesn't exist yet).
431    fn finish_pos(data: &Drawing, finish: &FinishTarget) -> Option<Pos2> {
432        match finish {
433            FinishTarget::Anchor(anchor) => data.anchor(*anchor),
434            FinishTarget::NewPin(marker) => Some(marker.center),
435        }
436    }
437
438    /// Resolve the cursor at `pos` to a finish target during routing: an existing
439    /// anchor (other than `start`) takes priority; failing that, the free slot's
440    /// marker a press there would land on — the same one the "(+)" and the
441    /// add-pin tool commit on, so no two tools disagree about where it is.
442    fn finish_at(data: &Drawing, start: PinId, pos: Pos2) -> Option<FinishTarget> {
443        match data.anchor_at_pos(pos) {
444            Some(anchor) if anchor != start => Some(FinishTarget::Anchor(anchor)),
445            Some(_) => None,
446            None => marker_at(&markers_in_scope(data), pos).map(FinishTarget::NewPin),
447        }
448    }
449
450    fn update_preview(&mut self, data: &mut Drawing) {
451        // Pull the routing request out as owned data first, so the `self.state`
452        // borrow ends before `preview_route` mutably borrows `self` (the cache).
453        // Only InProgress/Proposed route; idle/hover frames do no routing.
454        let req: Option<(PinId, Vec<Waypoint>, Pos2)> = match &self.state {
455            RouteToolState::InProgress {
456                start,
457                waypoints,
458                head,
459            } => Some((*start, waypoints.clone(), *head)),
460            RouteToolState::Proposed {
461                start,
462                waypoints,
463                finish,
464            } => Self::finish_pos(data, finish).map(|end| (*start, waypoints.clone(), end)),
465            _ => None,
466        };
467        self.preview_path = if let Some((start, waypoints, end)) = req {
468            if let Some(start_pos) = data.anchor(start) {
469                self.preview_route(
470                    data,
471                    &PreviewRequest {
472                        start,
473                        start_pos: snap_to_grid(start_pos),
474                        waypoints: &waypoints,
475                        end: snap_to_grid(end),
476                    },
477                )
478            } else {
479                self.preview_cache = None;
480                Vec::new()
481            }
482        } else {
483            // Gesture ended (idle/hover) — drop the cache so a new route (or a
484            // route drawn after this one commits) rebuilds against fresh state.
485            self.preview_cache = None;
486            Vec::new()
487        };
488    }
489
490    /// Route the in-progress route `start → waypoints → end` for the live preview,
491    /// reusing the cached router. `start_pos`/`end` are grid-snapped. The router is
492    /// (re)built only when its fixed inputs change — a different `start` (new route)
493    /// or a different waypoint count (a waypoint was added); the moving cursor
494    /// (`end`) does not change the graph and is routed via the existing L-path
495    /// fallback. Routes read-only (`SelfCost::Skip` semantics) so the cache never
496    /// accumulates the in-progress route's own occupancy between frames.
497    fn preview_route(&mut self, data: &Drawing, gesture: &PreviewRequest<'_>) -> Vec<TaggedPoint> {
498        let PreviewRequest {
499            start,
500            start_pos,
501            waypoints,
502            end,
503        } = *gesture;
504        let stale = match &self.preview_cache {
505            Some(c) => c.start != start || c.waypoint_count != waypoints.len(),
506            None => true,
507        };
508        if stale {
509            // Seed the fixed part of the in-progress route (start + waypoints) but
510            // NOT the moving head. Existing routes' occupancy is baked in by
511            // `scratch_closed_router`, so the preview still avoids existing wires.
512            let mut seeds: Vec<Point> = Vec::with_capacity(waypoints.len() + 1);
513            seeds.push(start_pos.into());
514            seeds.extend(waypoints.iter().map(waypoint_point));
515            let mut router = data.scratch_closed_router(&seeds);
516            let (wp_ids, wp_positions) = waypoint_nodes(waypoints);
517            // Route the fixed start→…→last-waypoint legs once; only the head leg
518            // is recomputed per frame below.
519            let fixed = route_fixed_legs(&mut router, &wp_positions, start_pos.into(), &wp_ids);
520            self.preview_cache = Some(Box::new(PreviewCache {
521                start,
522                waypoint_count: waypoints.len(),
523                router,
524                fixed,
525            }));
526        }
527        let Some(cache) = self.preview_cache.as_mut() else {
528            return Vec::new();
529        };
530        route_to_head(&mut cache.router, &cache.fixed, end.into())
531    }
532
533    /// While a route is in progress, surface the nearest block's new-pin markers
534    /// so the route can terminate on a not-yet-existent pin.
535    fn draw_new_pin_markers<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>) {
536        let Some(pointer) = Self::world_pointer(painter) else {
537            return;
538        };
539        let markers = nearest_block_markers(&markers_in_scope(data), pointer);
540        draw_route_markers(&markers, pointer, painter);
541    }
542
543    fn render<C: Canvas>(&self, data: &Drawing, painter: &mut Style<'_, C>) {
544        if matches!(
545            self.state,
546            RouteToolState::InProgress { .. } | RouteToolState::Proposed { .. }
547        ) {
548            Self::draw_new_pin_markers(data, painter);
549        }
550        match &self.state {
551            // Pre-routing: show the same animated anchor target as the select tool.
552            RouteToolState::Idle | RouteToolState::PinHeadHovered { .. } => {
553                Self::draw_hover_anchor_target(data, painter);
554            }
555            RouteToolState::InProgress {
556                start, waypoints, ..
557            } => {
558                let pts: Vec<Pos2> = self.preview_path.iter().map(|p| p.pos.into()).collect();
559                render_path_with_chamfered_corners(&pts)
560                    .render(painter, (0.5, Role::RouteInProgress));
561                for wp in waypoints {
562                    painter.circle_filled(px_point(wp.pos), PORT_RADIUS, Role::RouteInProgress);
563                }
564                Self::draw_end_target(data, *start, painter);
565            }
566            RouteToolState::Proposed {
567                start, waypoints, ..
568            } => {
569                let pts: Vec<Pos2> = self.preview_path.iter().map(|p| p.pos.into()).collect();
570                render_path_with_chamfered_corners(&pts)
571                    .render(painter, (1.5, Role::RouteInProgress));
572                // Mark where the route started; the end is shown by the grow-out
573                // target below (for an existing anchor) or the new-pin marker.
574                if let Some(pos) = data.anchor(*start) {
575                    painter.circle(
576                        pos,
577                        PORT_RADIUS,
578                        Role::RouteProposedEndpoint,
579                        (0.5, Role::RouteProposedEndpoint),
580                    );
581                }
582                for wp in waypoints {
583                    painter.circle_filled(px_point(wp.pos), PORT_RADIUS, Role::RouteInProgress);
584                }
585                Self::draw_end_target(data, *start, painter);
586            }
587        }
588    }
589
590    /// Grow the green route-end target on the anchor nearest the cursor (other than
591    /// `start`), mirroring the select tool's start affordance.
592    fn draw_end_target<C: Canvas>(data: &Drawing, start: PinId, painter: &mut Style<'_, C>) {
593        let Some(pointer) = Self::world_pointer(painter) else {
594            return;
595        };
596        route_start::draw_nearest_anchor_target(data, pointer, Some(start), painter);
597    }
598
599    /// Before a route starts, show the same animated anchor target the select tool's
600    /// route-start does, so hovering the route tool over a pin reads identically.
601    fn draw_hover_anchor_target<C: Canvas>(data: &Drawing, painter: &mut Style<'_, C>) {
602        let Some(pointer) = Self::world_pointer(painter) else {
603            return;
604        };
605        route_start::draw_nearest_anchor_target(data, pointer, None, painter);
606    }
607
608    fn world_pointer<C: Canvas>(painter: &Style<'_, C>) -> Option<Pos2> {
609        // Through the painter's pointer seam, so a scripted session's hover
610        // affordances follow the demo cursor, not the user's mouse.
611        painter.pointer_world()
612    }
613}