Skip to main content

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}