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