Skip to main content

blockworx/document/
auto_route.rs

1//! The route data model. [`AutoRoute`] holds a wire's *authored* state —
2//! anchors, waypoints, labels, name and accent — as pure data. The solved
3//! geometry (edges, resolved endpoints, crossing hops) is derived state,
4//! [`crate::presentation::RouteGeometry`], and the routing behaviour that
5//! produces it lives in [`crate::widget::auto_route`].
6
7use super::{GridPos, LineAnchor, LinearDistance, Waypoint};
8use crate::document::Lock;
9use crate::presentation::store::{IdMap, IdMapExt};
10use crate::store::{WaypointId, WireLabelId};
11
12/// Whether the wire reverses direction at `cur`: both the `prev → cur` and
13/// `cur → next` runs lie on the same axis (a shared row or a shared column) and
14/// point in opposite directions. A perpendicular turn (90°) or a straight run
15/// (0°, `cur` between its neighbours) is not a reversal.
16fn reverses_at(prev: GridPos, cur: GridPos, next: GridPos) -> bool {
17    let (ax, ay) = (cur.x - prev.x, cur.y - prev.y);
18    let (bx, by) = (next.x - cur.x, next.y - cur.y);
19    (ay == 0 && by == 0 && ax != 0 && bx != 0 && (ax > 0) != (bx > 0))
20        || (ax == 0 && bx == 0 && ay != 0 && by != 0 && (ay > 0) != (by > 0))
21}
22
23/// The waypoints a backtracking prune would drop from the path-ordered `seq`
24/// running `start → … → end`, without touching any route. Iterative, because
25/// dropping a corner can expose a reversal between its former neighbours. The
26/// policy core of [`AutoRoute::remove_backtracking_waypoints`], shared with the
27/// drag previews that must compute the prune without applying it.
28pub fn backtracking_waypoints(
29    mut seq: Vec<(WaypointId, GridPos)>,
30    start: GridPos,
31    end: GridPos,
32) -> Vec<WaypointId> {
33    let mut doomed = Vec::new();
34    loop {
35        let hit = (0..seq.len()).find(|&i| {
36            let prev = if i == 0 { start } else { seq[i - 1].1 };
37            let next = if i + 1 == seq.len() {
38                end
39            } else {
40                seq[i + 1].1
41            };
42            reverses_at(prev, seq[i].1, next)
43        });
44        match hit {
45            Some(i) => doomed.push(seq.remove(i).0),
46            None => break,
47        }
48    }
49    doomed
50}
51
52#[derive(PartialEq, Debug, Clone)]
53pub struct AutoRoute {
54    pub(crate) start: LineAnchor,
55    pub(crate) finish: LineAnchor,
56    pub(crate) waypoints: IdMap<WaypointId, Waypoint>,
57    pub(crate) labels: IdMap<WireLabelId, LinearDistance>,
58    pub(crate) name: String,
59    /// Optional accent index (`0..=7`) selecting the wire's stroke color via
60    /// [`Role::Accent0`]..[`Role::Accent7`] (see [`crate::theme::accent_role`]);
61    /// `None` uses the mode-based default.
62    ///
63    /// [`Role::Accent0`]: crate::theme::Role::Accent0
64    /// [`Role::Accent7`]: crate::theme::Role::Accent7
65    pub(crate) role: Option<u8>,
66}
67
68/// Pure data accessors and mutators. Anything that needs pixel space, the
69/// router, or the painter lives in `AutoRouteExt` (see the module docs).
70/// Which end of a route an operation works from.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum RouteEnd {
73    Start,
74    Finish,
75}
76
77impl AutoRoute {
78    /// A wire's authored state, freshly drawn: no name yet (so a new
79    /// label shows its placeholder), no accent.
80    pub fn new(
81        start: LineAnchor,
82        finish: LineAnchor,
83        waypoints: IdMap<WaypointId, Waypoint>,
84        labels: IdMap<WireLabelId, LinearDistance>,
85    ) -> Self {
86        Self {
87            start,
88            finish,
89            waypoints,
90            labels,
91            name: String::new(),
92            role: None,
93        }
94    }
95
96    pub fn start(&self) -> LineAnchor {
97        self.start
98    }
99    pub fn finish(&self) -> LineAnchor {
100        self.finish
101    }
102    /// Repoint both endpoints. Used by paste, which remaps a cloned route's
103    /// anchors onto the freshly assigned shape ids before re-routing.
104    pub fn set_anchors(&mut self, start: LineAnchor, finish: LineAnchor) {
105        self.start = start;
106        self.finish = finish;
107    }
108    /// Insert a waypoint at position `index` in path order (the corner
109    /// sequence), returning its id. Unlike `AutoRouteExt::add_waypoint` this does
110    /// no geometry-based reordering — the caller places the corner at a known
111    /// position, which is required now that waypoint insertion order *is* the
112    /// route's polyline order.
113    pub fn insert_waypoint_at(&mut self, index: usize, pos: GridPos, lock: Lock) -> WaypointId {
114        self.waypoints.insert_value_at(
115            index,
116            Waypoint {
117                pos,
118                locked: lock.is_locked(),
119            },
120        )
121    }
122    pub fn lock_waypoint(&mut self, id: WaypointId) {
123        if let Some(wp) = self.waypoint_mut(id) {
124            wp.locked = true;
125        }
126    }
127    pub fn iter_labels(&self) -> impl Iterator<Item = (WireLabelId, &LinearDistance)> {
128        self.labels.iter().map(|(&k, v)| (k, v))
129    }
130    pub fn label(&self, label_id: WireLabelId) -> Option<&LinearDistance> {
131        self.labels.get(&label_id)
132    }
133    pub fn label_mut(&mut self, label_id: WireLabelId) -> Option<&mut LinearDistance> {
134        self.labels.get_mut(&label_id)
135    }
136    pub fn remove_label(&mut self, label_id: WireLabelId) {
137        self.labels.shift_remove(&label_id);
138    }
139    pub fn waypoint(&self, waypoint_id: WaypointId) -> Option<&Waypoint> {
140        self.waypoints.get(&waypoint_id)
141    }
142    pub fn waypoint_mut(&mut self, waypoint_id: WaypointId) -> Option<&mut Waypoint> {
143        self.waypoints.get_mut(&waypoint_id)
144    }
145    pub fn update_waypoint(&mut self, waypoint_id: WaypointId, update: impl FnOnce(&mut Waypoint)) {
146        if let Some(wp) = self.waypoint_mut(waypoint_id) {
147            update(wp);
148        }
149    }
150    pub fn iter_waypoints(&self) -> impl Iterator<Item = (WaypointId, &Waypoint)> {
151        self.waypoints.iter().map(|(&k, v)| (k, v))
152    }
153    /// Discard every waypoint, so the next reroute autoroutes the endpoints on
154    /// Dijkstra cost alone, with no user-placed corners to route through.
155    pub fn clear_waypoints(&mut self) {
156        self.waypoints.clear();
157    }
158    /// The approach waypoints a trim from `end` would drop — up to two unlocked
159    /// corners nearest that endpoint, stopping at the first locked, hand-placed
160    /// corner so an explicit user bend survives. Pure twin of
161    /// [`Self::trim_approach_waypoints`]; drag previews route through the
162    /// skeleton minus this set without touching the stored waypoints.
163    pub fn approach_waypoint_ids(&self, end: RouteEnd) -> Vec<WaypointId> {
164        const APPROACH: usize = 2;
165        let ordered: Vec<WaypointId> = if end == RouteEnd::Start {
166            self.waypoints.keys().copied().collect()
167        } else {
168            self.waypoints.keys().rev().copied().collect()
169        };
170        let mut doomed = Vec::new();
171        for id in ordered.into_iter().take(APPROACH) {
172            if self.waypoints.get(&id).is_some_and(|wp| wp.locked) {
173                break;
174            }
175            doomed.push(id);
176        }
177        doomed
178    }
179
180    /// Drop up to two approach waypoints nearest a just-moved endpoint so the
181    /// next reroute builds a clean approach into the block instead of threading
182    /// the stale corners the drag left behind (which used to pin the approach
183    /// column and row to the block's old position). The policy — which corners
184    /// are doomed — is [`Self::approach_waypoint_ids`].
185    pub fn trim_approach_waypoints(&mut self, end: RouteEnd) {
186        for id in self.approach_waypoint_ids(end) {
187            self.waypoints.shift_remove(&id);
188        }
189    }
190    /// Remove any waypoint the wire doubles back on — a 180° turn *at* the
191    /// waypoint, where the run arriving and the run leaving lie on the same axis
192    /// in opposite directions. Strict waypoint routing produces these when a
193    /// waypoint sits "behind" its neighbours (the wire overshoots to it and comes
194    /// straight back); the autorouter itself never introduces a 180° turn, so a
195    /// reversal is always a stale corner worth dropping. Applies regardless of
196    /// `locked` — an explicit user corner that only makes the wire reverse is
197    /// still just a kink. `start`/`end` are the route's resolved endpoints (the
198    /// corners flanking the first and last waypoints). Returns whether anything
199    /// was removed, so the caller re-routes to erase the reversal geometry.
200    ///
201    /// Reruns after each removal because dropping a corner can expose a reversal
202    /// between the waypoints that were on either side of it.
203    pub fn remove_backtracking_waypoints(&mut self, start: GridPos, end: GridPos) -> bool {
204        let seq = self.waypoints.iter().map(|(&k, wp)| (k, wp.pos)).collect();
205        let doomed = backtracking_waypoints(seq, start, end);
206        let removed = !doomed.is_empty();
207        for id in doomed {
208            self.waypoints.shift_remove(&id);
209        }
210        removed
211    }
212    /// Shift every waypoint by `delta` grid cells. Used by paste to keep a
213    /// route's manual waypoints aligned with the endpoints when the whole
214    /// group is offset (a waypoint is stored in absolute grid space).
215    pub fn translate_waypoints(&mut self, delta: super::GridVec) {
216        for wp in self.waypoints.values_mut() {
217            wp.pos = wp.pos + delta;
218        }
219    }
220    pub fn route_name(&self) -> &str {
221        &self.name
222    }
223    pub fn set_route_name(&mut self, name: String) {
224        self.name = name;
225    }
226    /// The wire's accent index (`Some(0..=7)`), or `None` for the default color.
227    pub fn role(&self) -> Option<u8> {
228        self.role
229    }
230    /// Set the wire's accent index (`Some(0..=7)`) or clear it (`None`).
231    pub fn set_role(&mut self, role: Option<u8>) {
232        self.role = role;
233    }
234}