Skip to main content

blockworx_editor/widget/
waypoint_router.rs

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