1use blockworx_doc::geometry::{FracVal, GridPoint};
8use blockworx_geom::{Pos2, Vec2};
9
10use crate::grid::LINE_RADIUS;
11use crate::grid::px_point;
12use crate::presentation::store::{EdgeId, IdMap, IdMapExt};
13use crate::widget::edge::RouteEdgeExt;
14
15#[derive(Clone, PartialEq, Eq, Hash, Copy, Debug, PartialOrd, Ord)]
17pub enum RouteDirection {
18 Horizontal,
19 Vertical,
20}
21
22#[derive(Clone, PartialEq, Debug)]
26pub struct RouteEdge {
27 pub start: GridPoint,
28 pub end: GridPoint,
29}
30
31impl RouteEdge {
32 pub fn direction(&self) -> RouteDirection {
35 if (self.end.x - self.start.x).abs() > (self.end.y - self.start.y).abs() {
36 RouteDirection::Horizontal
37 } else {
38 RouteDirection::Vertical
39 }
40 }
41}
42
43#[derive(Clone, Copy, PartialEq, Debug)]
52pub struct Crossing {
53 pub pos: GridPoint,
54 pub orientation: RouteDirection,
55}
56
57#[derive(Clone, Copy, Debug)]
59pub struct LocAndDirection {
60 pub location: Pos2,
61 pub direction: RouteDirection,
62}
63
64#[derive(Default, Debug, Clone, PartialEq)]
65pub struct RouteGeometry {
66 pub edges: IdMap<EdgeId, RouteEdge>,
67 pub start_pos: GridPoint,
68 pub end_pos: GridPoint,
69 pub crossings: Vec<Crossing>,
70}
71
72impl RouteGeometry {
73 pub fn edge(&self, edge_index: EdgeId) -> Option<&RouteEdge> {
74 self.edges.get(&edge_index)
75 }
76
77 pub fn iter_edges(&self) -> impl Iterator<Item = (EdgeId, &RouteEdge)> {
78 self.edges.iter().map(|(&k, v)| (k, v))
79 }
80
81 pub fn start_pos(&self) -> Pos2 {
82 px_point(self.start_pos)
83 }
84
85 pub fn end_pos(&self) -> Pos2 {
86 px_point(self.end_pos)
87 }
88
89 pub fn points(&self) -> Vec<Pos2> {
92 let mut points: Vec<Pos2> = Vec::new();
93 points.push(px_point(self.start_pos));
94 for (_, edge) in self.iter_edges() {
95 points.push(px_point(edge.end));
96 }
97 points
98 }
99
100 pub fn hovered_corner(&self, hover_pos: Pos2) -> Option<(EdgeId, EdgeId)> {
101 self.edges.windows(2).find_map(|edges| {
102 let (edge_id1, edge1) = edges[0];
103 let (edge_id2, edge2) = edges[1];
104 if px_point(edge1.end).distance(hover_pos) <= LINE_RADIUS
105 && edge1.direction() != edge2.direction()
106 {
107 Some((edge_id1, edge_id2))
108 } else {
109 None
110 }
111 })
112 }
113
114 pub fn hovered_edge(&self, hover_pos: Pos2) -> Option<EdgeId> {
115 self.iter_edges().find_map(|(eid, edge)| {
116 if edge.distance(hover_pos).1 <= LINE_RADIUS
117 && px_point(edge.start).distance(hover_pos) > LINE_RADIUS
118 && px_point(edge.end).distance(hover_pos) > LINE_RADIUS
119 {
120 Some(eid)
121 } else {
122 None
123 }
124 })
125 }
126
127 pub fn hovered_edge_distance(&self, hover_pos: Pos2) -> Option<f32> {
134 if self.start_pos().distance(hover_pos) <= LINE_RADIUS
139 || self.end_pos().distance(hover_pos) <= LINE_RADIUS
140 {
141 return None;
142 }
143 self.iter_edges()
144 .filter_map(|(_, edge)| {
145 let perp = edge.distance(hover_pos).1;
146 (perp <= LINE_RADIUS).then_some(perp)
147 })
148 .min_by(f32::total_cmp)
149 }
150
151 pub fn map_linear_distance_to_position(&self, linear_distance: FracVal) -> LocAndDirection {
155 let mut distance: f32 = linear_distance.into();
156 for (_, edge) in self.iter_edges() {
157 if edge.length() < distance {
158 distance -= edge.length();
159 } else {
160 let frac = distance / edge.length();
161 let start: Pos2 = px_point(edge.start);
162 let end: Pos2 = px_point(edge.end);
163 return LocAndDirection {
164 location: start + frac * (end - start),
165 direction: edge.direction(),
166 };
167 }
168 }
169 LocAndDirection {
170 location: px_point(self.end_pos),
171 direction: RouteDirection::Horizontal,
172 }
173 }
174
175 pub fn slide_along(&self, from: FracVal, delta: Vec2) -> FracVal {
178 let anchor = self.map_linear_distance_to_position(from).location;
179 self.distance_along(anchor + delta)
180 }
181
182 pub fn distance_along(&self, pos: Pos2) -> FracVal {
184 let mut min_distance = f32::INFINITY;
185 let mut accum_distance: f32 = 0.0;
186 let mut min_distance_along: f32 = 0.0;
187 for (_, edge) in self.iter_edges() {
188 let (distance_along_edge, distance_to_point) = edge.distance(pos);
189 if distance_to_point < min_distance {
190 min_distance = distance_to_point;
191 min_distance_along = accum_distance + distance_along_edge;
192 }
193 accum_distance += edge.length();
194 }
195 FracVal::from(min_distance_along)
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::grid::GRID_SIZE;
203 use blockworx_geom::vec2;
204
205 fn horizontal_wire() -> RouteGeometry {
207 let start = GridPoint { x: 0, y: 0 };
208 let end = GridPoint { x: 10, y: 0 };
209 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
210 edges.insert_value(RouteEdge { start, end });
211 RouteGeometry {
212 edges,
213 start_pos: start,
214 end_pos: end,
215 crossings: Vec::new(),
216 }
217 }
218
219 #[test]
220 fn sliding_along_the_wire_advances_by_the_delta() {
221 let wire = horizontal_wire();
222 assert_eq!(wire.iter_edges().count(), 1, "one segment to slide along");
223 assert_eq!(
224 wire.start_pos().y,
225 wire.end_pos().y,
226 "the segment is horizontal"
227 );
228 let span = wire.end_pos().x - wire.start_pos().x;
229 assert!(
230 span > 5.0 * GRID_SIZE,
231 "long enough to slide two cells from three cells in without clamping: {span}"
232 );
233
234 let from = FracVal::from(3.0 * GRID_SIZE);
235 let slid = wire.slide_along(from, vec2(2.0 * GRID_SIZE, 0.0));
236
237 assert!(
238 (f32::from(slid) - 5.0 * GRID_SIZE).abs() < 0.01,
239 "sliding {} advanced to {}, expected {}",
240 2.0 * GRID_SIZE,
241 f32::from(slid),
242 5.0 * GRID_SIZE
243 );
244 }
245
246 #[test]
247 fn a_perpendicular_slide_reprojects_onto_the_wire() {
248 let wire = horizontal_wire();
249 let from = FracVal::from(3.0 * GRID_SIZE);
250 let anchor = wire.map_linear_distance_to_position(from).location;
251 assert!(
252 (anchor.y - wire.start_pos().y).abs() < 0.01,
253 "the starting anchor sits on the wire"
254 );
255
256 let slid = wire.slide_along(from, vec2(0.0, 4.0 * GRID_SIZE));
257 let landed = wire.map_linear_distance_to_position(slid).location;
258
259 assert!(
260 (landed.y - wire.start_pos().y).abs() < 0.01,
261 "an off-wire delta re-projects back onto the wire: {landed:?}"
262 );
263 assert!(
264 (landed.x - anchor.x).abs() < 0.01,
265 "and does not move along it: {} -> {}",
266 anchor.x,
267 landed.x
268 );
269 }
270}