Skip to main content

blockworx/derived/
route.rs

1//! Solved wire geometry: what the router produced for one route — its
2//! edge polyline, resolved endpoints, and crossing hops. Authored route
3//! state (anchors, waypoints, labels, name, role) stays on
4//! [`AutoRoute`](crate::document::AutoRoute); everything here is
5//! recomputable and never persisted.
6
7use egui::{Pos2, Vec2};
8
9use crate::document::{GridPos, LinearDistance, RouteDirection, RouteEdge};
10use crate::grid::LINE_RADIUS;
11use crate::store::{EdgeId, IdMap, IdMapExt};
12use crate::widget::edge::RouteEdgeExt;
13
14/// A purely visual decoration marking where this route crosses *over* another
15/// route. The crossing route gets a small semicircular "hop" drawn at `pos`;
16/// `orientation` is the direction of this route's own segment at the crossing
17/// (the bump bulges perpendicular to it). Every route crossing a given point in
18/// the point's chosen orientation carries a matching `Crossing`, so they all hop
19/// identically there. Crossings are recomputed from final geometry after routing
20/// (see [`crate::widget::auto_route::recompute_route_crossings`]) and are
21/// independent of `edges`, so edge-merge simplification never disturbs them.
22#[derive(Clone, Copy, PartialEq, Debug)]
23pub struct Crossing {
24    pub pos: GridPos,
25    pub orientation: RouteDirection,
26}
27
28/// Where a point on the route sits and which way the wire runs there.
29#[derive(Clone, Copy, Debug)]
30pub struct LocAndDirection {
31    pub location: Pos2,
32    pub direction: RouteDirection,
33}
34
35#[derive(Default, Debug, Clone, PartialEq)]
36pub struct RouteGeometry {
37    pub edges: IdMap<EdgeId, RouteEdge>,
38    pub start_pos: GridPos,
39    pub end_pos: GridPos,
40    pub crossings: Vec<Crossing>,
41}
42
43impl RouteGeometry {
44    pub fn edge(&self, edge_index: EdgeId) -> Option<&RouteEdge> {
45        self.edges.get(&edge_index)
46    }
47
48    pub fn iter_edges(&self) -> impl Iterator<Item = (EdgeId, &RouteEdge)> {
49        self.edges.iter().map(|(&k, v)| (k, v))
50    }
51
52    pub fn start_pos(&self) -> Pos2 {
53        self.start_pos.into()
54    }
55
56    pub fn end_pos(&self) -> Pos2 {
57        self.end_pos.into()
58    }
59
60    /// The polyline's corner sequence: the resolved start, then each
61    /// edge's end (no duplicated interior points).
62    pub fn points(&self) -> Vec<Pos2> {
63        let mut points: Vec<Pos2> = Vec::new();
64        points.push(self.start_pos.into());
65        for (_, edge) in self.iter_edges() {
66            points.push(edge.end.into());
67        }
68        points
69    }
70
71    pub fn hovered_corner(&self, hover_pos: Pos2) -> Option<(EdgeId, EdgeId)> {
72        self.edges.windows(2).find_map(|edges| {
73            let (edge_id1, edge1) = edges[0];
74            let (edge_id2, edge2) = edges[1];
75            if Pos2::from(edge1.end).distance(hover_pos) <= LINE_RADIUS
76                && edge1.direction() != edge2.direction()
77            {
78                Some((edge_id1, edge_id2))
79            } else {
80                None
81            }
82        })
83    }
84
85    pub fn hovered_edge(&self, hover_pos: Pos2) -> Option<EdgeId> {
86        self.iter_edges().find_map(|(eid, edge)| {
87            if edge.distance(hover_pos).1 <= LINE_RADIUS
88                && Pos2::from(edge.start).distance(hover_pos) > LINE_RADIUS
89                && Pos2::from(edge.end).distance(hover_pos) > LINE_RADIUS
90            {
91                Some(eid)
92            } else {
93                None
94            }
95        })
96    }
97
98    /// Perpendicular distance from `hover_pos` to the nearest point on the
99    /// route, or `None` if the point isn't within the hit radius (or sits
100    /// on a terminal endpoint, which is left to the pin under it). Unlike
101    /// [`hovered_edge`](Self::hovered_edge) this keeps interior corners
102    /// hoverable. Lets a caller pick the *closest* route among several in
103    /// range.
104    pub fn hovered_edge_distance(&self, hover_pos: Pos2) -> Option<f32> {
105        // Only the route's two terminal endpoints are excluded, so a hover near a
106        // pin falls through to it. Interior corners are NOT excluded: the segment
107        // distance already clamps to the bend, so a corner stays hoverable instead
108        // of sitting in a dead zone where both adjacent edges reject it.
109        if self.start_pos().distance(hover_pos) <= LINE_RADIUS
110            || self.end_pos().distance(hover_pos) <= LINE_RADIUS
111        {
112            return None;
113        }
114        self.iter_edges()
115            .filter_map(|(_, edge)| {
116                let perp = edge.distance(hover_pos).1;
117                (perp <= LINE_RADIUS).then_some(perp)
118            })
119            .min_by(f32::total_cmp)
120    }
121
122    // Convert a distance along the route to a point on the route. This is
123    // the inverse of `distance_along`. If the distance is out of range,
124    // take the end anchor.
125    pub fn map_linear_distance_to_position(
126        &self,
127        linear_distance: LinearDistance,
128    ) -> LocAndDirection {
129        let mut distance: f32 = linear_distance.into();
130        for (_, edge) in self.iter_edges() {
131            if edge.length() < distance {
132                distance -= edge.length();
133            } else {
134                let frac = distance / edge.length();
135                let start: Pos2 = edge.start.into();
136                let end: Pos2 = edge.end.into();
137                return LocAndDirection {
138                    location: start + frac * (end - start),
139                    direction: edge.direction(),
140                };
141            }
142        }
143        LocAndDirection {
144            location: self.end_pos.into(),
145            direction: RouteDirection::Horizontal,
146        }
147    }
148
149    /// Slide an arc-length position along the route by a screen-space delta:
150    /// map to world, offset, re-project onto the nearest point of the wire.
151    pub fn slide_along(&self, from: LinearDistance, delta: Vec2) -> LinearDistance {
152        let anchor = self.map_linear_distance_to_position(from).location;
153        self.distance_along(anchor + delta)
154    }
155
156    /// The arc length along the route of the nearest point to `pos`.
157    pub fn distance_along(&self, pos: Pos2) -> LinearDistance {
158        let mut min_distance = f32::INFINITY;
159        let mut accum_linear_distance = LinearDistance::from(0.0_f64);
160        let mut min_linear_distance = LinearDistance::from(0.0_f64);
161        for (_, edge) in self.iter_edges() {
162            let (lin_distance, distance_to_point) = edge.distance(pos);
163            if distance_to_point < min_distance {
164                min_distance = distance_to_point;
165                min_linear_distance = accum_linear_distance + lin_distance;
166            }
167            accum_linear_distance += edge.length();
168        }
169        min_linear_distance
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::grid::GRID_SIZE;
177    use egui::vec2;
178
179    /// One straight horizontal run, ten cells long.
180    fn horizontal_wire() -> RouteGeometry {
181        let start = GridPos { x: 0, y: 0 };
182        let end = GridPos { x: 10, y: 0 };
183        let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
184        edges.insert_value(RouteEdge { start, end });
185        RouteGeometry {
186            edges,
187            start_pos: start,
188            end_pos: end,
189            crossings: Vec::new(),
190        }
191    }
192
193    #[test]
194    fn sliding_along_the_wire_advances_by_the_delta() {
195        let wire = horizontal_wire();
196        assert_eq!(wire.iter_edges().count(), 1, "one segment to slide along");
197        assert_eq!(
198            wire.start_pos().y,
199            wire.end_pos().y,
200            "the segment is horizontal"
201        );
202        let span = wire.end_pos().x - wire.start_pos().x;
203        assert!(
204            span > 5.0 * GRID_SIZE,
205            "long enough to slide two cells from three cells in without clamping: {span}"
206        );
207
208        let from = LinearDistance::from(3.0 * GRID_SIZE);
209        let slid = wire.slide_along(from, vec2(2.0 * GRID_SIZE, 0.0));
210
211        assert!(
212            (f32::from(slid) - 5.0 * GRID_SIZE).abs() < 0.01,
213            "sliding {} advanced to {}, expected {}",
214            2.0 * GRID_SIZE,
215            f32::from(slid),
216            5.0 * GRID_SIZE
217        );
218    }
219
220    #[test]
221    fn a_perpendicular_slide_reprojects_onto_the_wire() {
222        let wire = horizontal_wire();
223        let from = LinearDistance::from(3.0 * GRID_SIZE);
224        let anchor = wire.map_linear_distance_to_position(from).location;
225        assert!(
226            (anchor.y - wire.start_pos().y).abs() < 0.01,
227            "the starting anchor sits on the wire"
228        );
229
230        let slid = wire.slide_along(from, vec2(0.0, 4.0 * GRID_SIZE));
231        let landed = wire.map_linear_distance_to_position(slid).location;
232
233        assert!(
234            (landed.y - wire.start_pos().y).abs() < 0.01,
235            "an off-wire delta re-projects back onto the wire: {landed:?}"
236        );
237        assert!(
238            (landed.x - anchor.x).abs() < 0.01,
239            "and does not move along it: {} -> {}",
240            anchor.x,
241            landed.x
242        );
243    }
244}