Skip to main content

blockworx/widget/
waypoint_router.rs

1use std::collections::BTreeMap;
2
3use blockworx_router::{
4    ClosedRouter, Direction, WIRE_COST, cost::Cost, direction_between, point::Point,
5};
6
7use crate::{
8    edit::create::PathOrdinal,
9    widget::{edge::RouteEdge, segmentkind::SegmentKind},
10};
11
12/// Route one leg, but PREFER a straight axis-aligned segment whenever one is
13/// legal: if `a` and `b` share a row or column and the direct wire neither crosses
14/// nor hugs a block, take it — even if the pathfinder could find a lower-cost
15/// detour around existing wires. Legs that are non-colinear, cross a block, or
16/// would hug one (run alongside an edge within the routing gutter) fall back to
17/// [`ClosedRouter::route_leg`], which bows out into the gutter. This keeps a
18/// hand-placed waypoint path literal: a straight run between two waypoints stays
19/// straight.
20fn route_leg_preferring_straight(
21    router: &mut ClosedRouter,
22    a: Point,
23    b: Point,
24    incoming: Option<Direction>,
25) -> (Vec<Point>, Option<Direction>) {
26    if a != b
27        && (a.x == b.x || a.y == b.y)
28        && !router.is_wire_blocked(a, b)
29        && !router.wire_hugs_block(a, b)
30    {
31        (vec![a, b], direction_between(a, b))
32    } else {
33        router.route_leg(a, b, incoming)
34    }
35}
36
37#[derive(Copy, Clone)]
38pub struct TaggedPoint {
39    pub segment: SegmentKind,
40    pub pos: Point,
41}
42
43impl std::fmt::Debug for TaggedPoint {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self.segment {
46            SegmentKind::StartToEnd => write!(f, "s->e {}", self.pos),
47            SegmentKind::StartToWaypoint(wp) => {
48                write!(f, "s->wp[{}] {}", usize::from(wp), self.pos)
49            }
50            SegmentKind::WaypointToWaypoint(wp0, wp1) => {
51                write!(
52                    f,
53                    "wp[{}]->wp[{}] {}",
54                    usize::from(wp0),
55                    usize::from(wp1),
56                    self.pos
57                )
58            }
59            SegmentKind::WaypointToEnd(wp) => {
60                write!(f, "wp[{}] -> e {}", usize::from(wp), self.pos)
61            }
62        }
63    }
64}
65
66// ── Closed-router bridge ────────────────────────────────────────────────────
67// The closed router is store-agnostic (it knows only `Point`s), so these
68// widget-layer free functions carry the `PathOrdinal`/`RouteEdge` types across the
69// boundary, keeping the router/widget layering clean.
70
71/// Route `start → each waypoint (by id) → end` on a [`ClosedRouter`], resolving
72/// waypoint positions through `wp_positions`. Adds NO geometry (channels were
73/// seeded at build): each leg is routed with [`ClosedRouter::route_leg`].
74///
75/// When `apply_self_cost` is true, each leg's `WIRE_COST` occupancy is applied in
76/// place with [`ClosedRouter::bump_leg`] so the route's later legs spread off its
77/// earlier ones — used when committing/routing a real route. When false, the
78/// router is left byte-identical (read-only routing) — used for the live `RouteTool`
79/// preview against a cached router that must not accumulate the in-progress route's
80/// own occupancy between frames. `wp_positions` must contain every id in `wp_ids`.
81/// Whether a routed leg adds its own occupancy to the graph, so later legs of
82/// the same route avoid it. The live preview skips it: it re-routes the head
83/// leg every frame and must not accumulate cost.
84///
85/// Only the router's own comparison test builds the `Skip` form today: the live
86/// preview reaches the same result through [`route_fixed_legs`] + [`route_to_head`]
87/// rather than this entry point.
88#[cfg_attr(
89    not(test),
90    allow(dead_code, reason = "Skip is exercised by tests only")
91)]
92#[derive(Clone, Copy, PartialEq, Eq)]
93pub enum SelfCost {
94    Apply,
95    Skip,
96}
97
98/// A route to lay out: its two endpoints and the waypoints in between (ids in
99/// order, positions by id), plus whether each routed leg adds its own occupancy.
100pub struct RouteRequest<'a> {
101    pub start: Point,
102    pub end: Point,
103    pub wp_ids: &'a [PathOrdinal],
104    pub wp_positions: &'a BTreeMap<PathOrdinal, Point>,
105    pub self_cost: SelfCost,
106}
107
108impl RouteRequest<'_> {
109    pub fn route(&self, router: &mut ClosedRouter) -> Vec<TaggedPoint> {
110        let Self {
111            start,
112            end,
113            wp_ids,
114            wp_positions,
115            self_cost,
116        } = *self;
117        let bump = |router: &mut ClosedRouter, subpath: &[Point]| {
118            if self_cost == SelfCost::Apply {
119                router.bump_leg(subpath, WIRE_COST);
120            }
121        };
122        let mut path = Vec::new();
123        let mut incoming: Option<Direction> = None;
124        let Some(&first_id) = wp_ids.first() else {
125            // No waypoints: a single start → end leg.
126            let (subpath, _) = route_leg_preferring_straight(router, start, end, incoming);
127            bump(router, &subpath);
128            return subpath
129                .into_iter()
130                .map(|pos| TaggedPoint {
131                    pos,
132                    segment: SegmentKind::StartToEnd,
133                })
134                .collect();
135        };
136
137        let (subpath, outgoing) =
138            route_leg_preferring_straight(router, start, wp_positions[&first_id], incoming);
139        incoming = outgoing;
140        bump(router, &subpath);
141        path.extend(subpath.into_iter().map(|pos| TaggedPoint {
142            pos,
143            segment: SegmentKind::StartToWaypoint(first_id),
144        }));
145
146        for w in wp_ids.windows(2) {
147            let (a_id, b_id) = (w[0], w[1]);
148            let (subpath, outgoing) = route_leg_preferring_straight(
149                router,
150                wp_positions[&a_id],
151                wp_positions[&b_id],
152                incoming,
153            );
154            incoming = outgoing;
155            bump(router, &subpath);
156            path.extend(subpath.into_iter().map(|pos| TaggedPoint {
157                pos,
158                segment: SegmentKind::WaypointToWaypoint(a_id, b_id),
159            }));
160        }
161
162        let last_id = wp_ids.last().copied().unwrap_or(first_id);
163        let (subpath, _) =
164            route_leg_preferring_straight(router, wp_positions[&last_id], end, incoming);
165        bump(router, &subpath);
166        path.extend(subpath.into_iter().map(|pos| TaggedPoint {
167            pos,
168            segment: SegmentKind::WaypointToEnd(last_id),
169        }));
170        path
171    }
172}
173
174/// The fixed portion of an in-progress route — `start → each waypoint → the last
175/// waypoint` — computed once and reused while only the cursor (`head`) moves. Split
176/// out of [`RouteRequest::route`] so the live `RouteTool` preview recomputes only the
177/// trailing head leg per frame. Read-only (no occupancy applied).
178pub struct FixedLegs {
179    /// Tagged points for `start → … → last waypoint`; empty when there are no
180    /// waypoints (then the whole route is the single head leg).
181    path: Vec<TaggedPoint>,
182    /// Where the trailing head leg starts: the last waypoint, or `start`.
183    tail: Point,
184    /// The last waypoint id (head leg is `WaypointToEnd`) or `None` (`StartToEnd`).
185    tail_wp: Option<PathOrdinal>,
186    /// Direction of travel arriving at `tail`, threaded into the head leg so it
187    /// cannot double back.
188    incoming: Option<Direction>,
189}
190
191/// Route the fixed `start → … → last waypoint` portion of an in-progress route.
192/// Identical to the corresponding legs of [`RouteRequest::route`] with
193/// [`SelfCost::Skip`]. `wp_positions` must contain every id in `wp_ids`.
194pub fn route_fixed_legs(
195    router: &mut ClosedRouter,
196    wp_positions: &BTreeMap<PathOrdinal, Point>,
197    start: Point,
198    wp_ids: &[PathOrdinal],
199) -> FixedLegs {
200    let mut path = Vec::new();
201    let mut incoming: Option<Direction> = None;
202    let Some(&first_id) = wp_ids.first() else {
203        return FixedLegs {
204            path,
205            tail: start,
206            tail_wp: None,
207            incoming,
208        };
209    };
210    let (subpath, outgoing) =
211        route_leg_preferring_straight(router, start, wp_positions[&first_id], incoming);
212    incoming = outgoing;
213    path.extend(subpath.into_iter().map(|pos| TaggedPoint {
214        pos,
215        segment: SegmentKind::StartToWaypoint(first_id),
216    }));
217    for w in wp_ids.windows(2) {
218        let (a_id, b_id) = (w[0], w[1]);
219        let (subpath, outgoing) = route_leg_preferring_straight(
220            router,
221            wp_positions[&a_id],
222            wp_positions[&b_id],
223            incoming,
224        );
225        incoming = outgoing;
226        path.extend(subpath.into_iter().map(|pos| TaggedPoint {
227            pos,
228            segment: SegmentKind::WaypointToWaypoint(a_id, b_id),
229        }));
230    }
231    let last_id = wp_ids.last().copied().unwrap_or(first_id);
232    FixedLegs {
233        path,
234        tail: wp_positions[&last_id],
235        tail_wp: Some(last_id),
236        incoming,
237    }
238}
239
240/// Append the trailing head leg (`fixed.tail → head`) to the fixed legs — the only
241/// part that changes as the cursor moves. `head` is typically not a graph node, so
242/// this leg is the L-path fallback. Together with [`route_fixed_legs`] this equals
243/// [`RouteRequest::route`] with [`SelfCost::Skip`].
244pub fn route_to_head(
245    router: &mut ClosedRouter,
246    fixed: &FixedLegs,
247    head: Point,
248) -> Vec<TaggedPoint> {
249    let (subpath, _) = route_leg_preferring_straight(router, fixed.tail, head, fixed.incoming);
250    let segment = match fixed.tail_wp {
251        Some(id) => SegmentKind::WaypointToEnd(id),
252        None => SegmentKind::StartToEnd,
253    };
254    let mut path = fixed.path.clone();
255    path.extend(subpath.into_iter().map(|pos| TaggedPoint { segment, pos }));
256    path
257}
258
259/// Occupy a completed route on a [`ClosedRouter`] by adding `cost` to the graph
260/// edges each wire covers (the in-place analogue of the old segment-based occupancy).
261pub fn add_route_cost<'a>(
262    router: &mut ClosedRouter,
263    edges: impl Iterator<Item = &'a RouteEdge>,
264    cost: Cost,
265) {
266    for edge in edges {
267        router.add_wire_cost(edge.start.into(), edge.end.into(), cost);
268    }
269}
270
271/// Whether any of a route's edges crosses a blocked rectangle on a [`ClosedRouter`]
272/// (read-only; no rebuild).
273pub fn route_edges_blocked<'a>(
274    router: &ClosedRouter,
275    mut edges: impl Iterator<Item = &'a RouteEdge>,
276) -> bool {
277    edges.any(|edge| router.is_wire_blocked(edge.start.into(), edge.end.into()))
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use blockworx_router::RouterNGBuilder;
284    use blockworx_router::point::point;
285
286    #[test]
287    fn a_legal_straight_leg_is_taken_verbatim() {
288        // Two colinear points with a clear path between them: the leg must be the
289        // straight segment, NOT a lower-cost router detour. This is the behaviour a
290        // hand-placed waypoint path relies on.
291        let a = point(0, 0);
292        let b = point(10, 0);
293        let mut builder = RouterNGBuilder::default();
294        builder.add_seed_point(a);
295        builder.add_seed_point(b);
296        let mut router = builder.build_closed();
297
298        let (path, dir) = route_leg_preferring_straight(&mut router, a, b, None);
299        assert_eq!(path, vec![a, b], "a legal straight leg is taken verbatim");
300        assert_eq!(dir, Some(Direction::East));
301    }
302
303    #[test]
304    fn a_hugging_straight_leg_routes_around_the_block() {
305        // Two colinear points one cell off the block's East edge (x = right + 1):
306        // the straight run hugs the block's right side, so it must bow out into the
307        // gutter rather than be taken verbatim.
308        let mut builder = RouterNGBuilder::default();
309        builder.add_block(point(0, 0), point(10, 10));
310        let a = point(11, 2);
311        let b = point(11, 8);
312        builder.add_seed_point(a);
313        builder.add_seed_point(b);
314        let mut router = builder.build_closed();
315
316        let (path, _) = route_leg_preferring_straight(&mut router, a, b, None);
317        assert_ne!(
318            path,
319            vec![a, b],
320            "a hugging straight leg must not stay verbatim"
321        );
322        assert!(path.len() > 2, "the leg bowed out around the block");
323    }
324}