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}