1pub use crate::presentation::{Crossing, RouteGeometry};
2use blockworx_geom::{Pos2, Rect, vec2};
6use blockworx_paint::Renderer;
7use blockworx_router::{ClosedRouter, Resolution, WIRE_COST, point::Point};
8
9use std::collections::BTreeMap;
10
11use blockworx_doc::{
12 document::{IndexedDocument, chronological},
13 geometry::{FracVal, GridPoint, GridVec, Waypoint},
14 id::{RouteId, RouteLabelId},
15};
16
17use crate::{
18 edit::create::PathOrdinal,
19 grid::{SHIM, px_point},
20 presentation::{
21 RouteDirection,
22 store::{IdMap, IdMapExt},
23 },
24 theme::Style,
25 widget::{
26 edge::RouteEdge,
27 reconstruct::Endpoints,
28 segmentkind::SegmentKind,
29 waypoint_router::{RouteRequest, SelfCost, TaggedPoint, add_route_cost},
30 },
31};
32
33#[derive(Clone, Copy, Debug)]
36pub struct HEdge {
37 pub order: usize,
38 pub y: i32,
39 pub x_lo: i32,
40 pub x_hi: i32,
41}
42
43#[derive(Clone, Copy, Debug)]
46pub struct VEdge {
47 pub order: usize,
48 pub x: i32,
49 pub y_lo: i32,
50 pub y_hi: i32,
51}
52
53#[derive(Default)]
57struct CrossingSite {
58 h_routes: std::collections::BTreeSet<usize>,
59 v_routes: std::collections::BTreeSet<usize>,
60}
61
62pub fn compute_crossings(
74 h_edges: &[HEdge],
75 v_edges: &[VEdge],
76 num_routes: usize,
77) -> Vec<Vec<Crossing>> {
78 use blockworx_router::event::{Event, EventSense};
79 use std::collections::BTreeMap;
80 use std::ops::Bound;
81
82 let mut sites: BTreeMap<(i32, i32), CrossingSite> = BTreeMap::new();
84
85 let mut events: Vec<Event<i32, usize>> = Vec::with_capacity(h_edges.len() * 2 + v_edges.len());
94 for (i, h) in h_edges.iter().enumerate() {
95 events.push(Event::enter(h.x_lo, i));
96 events.push(Event::exit(h.x_hi, i));
97 }
98 for (j, v) in v_edges.iter().enumerate() {
99 events.push(Event::scan(v.x, j));
100 }
101 events.sort();
102
103 let mut active: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
105 for event in events {
106 match event.sense() {
107 EventSense::Enter => {
108 let i = event.cost();
109 active.entry(h_edges[i].y).or_default().push(i);
110 }
111 EventSense::Exit => {
112 let i = event.cost();
113 if let Some(row) = active.get_mut(&h_edges[i].y) {
114 if let Some(k) = row.iter().position(|&e| e == i) {
115 row.swap_remove(k);
116 }
117 if row.is_empty() {
118 active.remove(&h_edges[i].y);
119 }
120 }
121 }
122 EventSense::Scan => {
123 let v = &v_edges[event.cost()];
124 for (_y, row) in active.range((Bound::Excluded(v.y_lo), Bound::Excluded(v.y_hi))) {
125 for &i in row {
126 let h = &h_edges[i];
127 if h.x_lo < v.x && v.x < h.x_hi && h.order != v.order {
129 let site = sites.entry((v.x, h.y)).or_default();
130 site.h_routes.insert(h.order);
131 site.v_routes.insert(v.order);
132 }
133 }
134 }
135 }
136 }
137 }
138
139 let mut out = vec![Vec::new(); num_routes];
142 for ((x, y), site) in sites {
143 let pos = GridPoint { x, y };
144 let lone_pair = site.v_routes.iter().next().zip(site.h_routes.iter().next());
147 let vertical = match lone_pair {
148 Some((v, h)) if site.h_routes.len() == 1 && site.v_routes.len() == 1 => v > h,
149 _ => true,
150 };
151 let (orientation, hoppers) = if vertical {
152 (RouteDirection::Vertical, site.v_routes)
153 } else {
154 (RouteDirection::Horizontal, site.h_routes)
155 };
156 for order in hoppers {
157 out[order].push(Crossing { pos, orientation });
158 }
159 }
160 out
161}
162
163pub struct Wire<'a> {
169 pub route: &'a blockworx_doc::block_model::Route,
170 pub labels: Vec<(RouteLabelId, FracVal)>,
171}
172
173pub fn route_labels(indexed: &IndexedDocument<'_>, route: RouteId) -> Vec<(RouteLabelId, FracVal)> {
178 let Some(entry) = indexed.index.routes.get(&route) else {
179 return Vec::new();
180 };
181 let held: Vec<(RouteLabelId, &blockworx_doc::block_model::RouteLabel)> = entry
182 .labels
183 .iter()
184 .filter_map(|&id| Some((id, indexed.doc.route_label(&id)?)))
185 .collect();
186 chronological(held.iter().copied())
187 .into_iter()
188 .filter_map(|id| {
189 let label = indexed.doc.route_label(&id)?;
190 Some((id, label.pos))
191 })
192 .collect()
193}
194
195pub fn hit_waypoint(waypoints: &[Waypoint], pos: Pos2, tolerance: f32) -> Option<PathOrdinal> {
199 waypoints.iter().enumerate().find_map(|(index, wp)| {
200 (px_point(wp.pos).distance(pos) <= tolerance).then(|| PathOrdinal::new(index))
201 })
202}
203
204pub fn hit_text_anchor<R: Renderer>(
208 name: &str,
209 labels: &[(RouteLabelId, FracVal)],
210 geometry: &RouteGeometry,
211 hover_pos: Pos2,
212 painter: &Style<'_, R>,
213) -> Option<RouteLabelId> {
214 let text = if name.is_empty() {
215 crate::render::ADD_ROUTE_LABEL_PLACEHOLDER
216 } else {
217 name
218 };
219 let measured = painter.text_size(text, &painter.theme().route_font);
220 labels.iter().find_map(|&(lid, dist)| {
221 let pos_and_direction = geometry.map_linear_distance_to_position(dist);
222 let center_of_label = pos_and_direction.location
223 + match pos_and_direction.direction {
224 RouteDirection::Horizontal => vec2(0.0, -measured.y / 2.0 - SHIM / 4.0),
225 RouteDirection::Vertical => vec2(measured.y / 2.0 + SHIM / 4.0, 0.0),
226 };
227 let label_size = match pos_and_direction.direction {
228 RouteDirection::Horizontal => measured,
229 RouteDirection::Vertical => vec2(measured.y, measured.x),
230 };
231 Rect::from_center_size(center_of_label, label_size)
232 .contains(hover_pos)
233 .then_some(lid)
234 })
235}
236
237pub fn text_anchors(labels: &[(RouteLabelId, FracVal)], geometry: &RouteGeometry) -> Vec<Pos2> {
239 labels
240 .iter()
241 .map(|&(_, dist)| geometry.map_linear_distance_to_position(dist).location)
242 .collect()
243}
244
245pub fn label_anchors(
249 labels: &[(RouteLabelId, FracVal)],
250 geometry: &RouteGeometry,
251) -> Vec<(RouteLabelId, Pos2)> {
252 labels
253 .iter()
254 .map(|&(lid, dist)| (lid, geometry.map_linear_distance_to_position(dist).location))
255 .collect()
256}
257
258pub fn reanchored(
263 geometry: &RouteGeometry,
264 anchors: &[(RouteLabelId, Pos2)],
265) -> Vec<(RouteLabelId, FracVal)> {
266 anchors
267 .iter()
268 .map(|&(lid, pos)| (lid, geometry.distance_along(pos)))
269 .collect()
270}
271
272pub fn geometry_from_points(points: &[TaggedPoint]) -> RouteGeometry {
277 let mut edges: Vec<(RouteEdge, SegmentKind)> = Vec::new();
278 for windows in points.windows(2) {
279 let start = windows[0];
280 let end = windows[1];
281 if start.segment != end.segment {
282 continue;
283 }
284 edges.push((
285 RouteEdge {
286 start: start.pos.into(),
287 end: end.pos.into(),
288 },
289 start.segment,
290 ));
291 }
292 let start_pos = points
293 .first()
294 .map_or(GridPoint::default(), |p| GridPoint::from(p.pos));
295 let end_pos = points
296 .last()
297 .map_or(GridPoint::default(), |p| GridPoint::from(p.pos));
298 let mut merged_edges = IdMap::default();
299 let mut current_edge: Option<(RouteEdge, SegmentKind)> = None;
300 for (edge, kind) in edges {
301 if let Some((current, current_kind)) = &mut current_edge {
302 if *current_kind == kind && current.direction() == edge.direction() {
303 current.end = edge.end;
304 } else {
305 merged_edges.insert_value(current.clone());
306 current_edge = Some((edge, kind));
307 }
308 } else {
309 current_edge = Some((edge, kind));
310 }
311 }
312 if let Some((current, _)) = current_edge {
313 merged_edges.insert_value(current);
314 }
315 RouteGeometry {
316 edges: merged_edges,
317 start_pos,
318 end_pos,
319 }
320}
321
322pub fn reroute_preview_excluding(
330 waypoints: &[Waypoint],
331 geometry: &mut RouteGeometry,
332 ends: Endpoints,
333 excluded: &std::collections::HashSet<PathOrdinal>,
334 router: &mut ClosedRouter,
335) -> Resolution {
336 let mut unique_positions = std::collections::HashSet::new();
337 let mut wp_ids: Vec<PathOrdinal> = Vec::new();
338 let mut wp_positions: BTreeMap<PathOrdinal, Point> = BTreeMap::new();
339 for (index, wp) in waypoints.iter().enumerate() {
340 let id = PathOrdinal::new(index);
341 let pos = wp.pos;
342 if excluded.contains(&id)
343 || !router.is_accessible(Point::from(pos))
344 || !unique_positions.insert(pos)
345 {
346 continue;
347 }
348 wp_ids.push(id);
349 wp_positions.insert(id, Point::from(pos));
350 }
351 let laid = RouteRequest {
352 start: ends.start.into(),
353 end: ends.end.into(),
354 wp_ids: &wp_ids,
355 wp_positions: &wp_positions,
356 self_cost: SelfCost::Apply,
357 }
358 .route(router);
359 *geometry = geometry_from_points(&laid.points);
360 geometry.start_pos = ends.start;
361 geometry.end_pos = ends.end;
362 add_route_cost(
363 router,
364 geometry.edges.iter().map(|(_, edge)| edge),
365 WIRE_COST,
366 );
367 laid.resolution
368}
369
370pub fn reroute_preview_closed(
376 waypoints: &[Waypoint],
377 geometry: &mut RouteGeometry,
378 ends: Endpoints,
379 waypoint_delta: GridVec,
380 router: &mut ClosedRouter,
381) -> Resolution {
382 let offset_positions: BTreeMap<PathOrdinal, Point> = waypoints
386 .iter()
387 .enumerate()
388 .map(|(index, wp)| {
389 (
390 PathOrdinal::new(index),
391 Point::from(wp.pos + waypoint_delta),
392 )
393 })
394 .collect();
395 let wp_ids: Vec<PathOrdinal> = (0..waypoints.len()).map(PathOrdinal::new).collect();
396 let laid = RouteRequest {
397 start: ends.start.into(),
398 end: ends.end.into(),
399 wp_ids: &wp_ids,
400 wp_positions: &offset_positions,
401 self_cost: SelfCost::Apply,
402 }
403 .route(router);
404 let preview = geometry_from_points(&laid.points);
405 add_route_cost(
406 router,
407 preview.edges.iter().map(|(_, edge)| edge),
408 WIRE_COST,
409 );
410 geometry.edges = preview.edges;
411 geometry.start_pos = ends.start;
412 geometry.end_pos = ends.end;
413 laid.resolution
414}
415
416pub fn route_hops<'a>(routes: impl IntoIterator<Item = &'a RouteGeometry>) -> Vec<Vec<Crossing>> {
427 let mut h_edges: Vec<HEdge> = Vec::new();
428 let mut v_edges: Vec<VEdge> = Vec::new();
429 let mut num_routes = 0;
430 for (order, geometry) in routes.into_iter().enumerate() {
431 num_routes = order + 1;
432 for (_, edge) in geometry.iter_edges() {
433 let (s, e) = (edge.start, edge.end);
434 if s.y == e.y && s.x != e.x {
435 h_edges.push(HEdge {
436 order,
437 y: s.y,
438 x_lo: s.x.min(e.x),
439 x_hi: s.x.max(e.x),
440 });
441 } else if s.x == e.x && s.y != e.y {
442 v_edges.push(VEdge {
443 order,
444 x: s.x,
445 y_lo: s.y.min(e.y),
446 y_hi: s.y.max(e.y),
447 });
448 }
449 }
450 }
451 compute_crossings(&h_edges, &v_edges, num_routes)
452}
453
454#[cfg(test)]
455mod tests {
456 use super::{
457 Crossing, HEdge, RouteGeometry, VEdge, compute_crossings, label_anchors, reanchored,
458 };
459 use crate::presentation::{
460 RouteDirection, RouteEdge,
461 store::{EdgeId, IdMap, IdMapExt},
462 };
463 use blockworx_doc::{
464 fixtures::route_label_id,
465 geometry::{FracVal, GridPoint},
466 id::RouteLabelId,
467 };
468 use blockworx_geom::{Pos2, pos2};
469
470 fn single_edge_geometry(start: GridPoint, end: GridPoint) -> RouteGeometry {
473 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
474 edges.insert_value(RouteEdge { start, end });
475 RouteGeometry {
476 edges,
477 start_pos: start,
478 end_pos: end,
479 }
480 }
481
482 fn label_at(
485 geometry: &RouteGeometry,
486 pos: Pos2,
487 ) -> (RouteLabelId, Vec<(RouteLabelId, FracVal)>) {
488 let lid = route_label_id(1);
489 (lid, vec![(lid, geometry.distance_along(pos))])
490 }
491
492 #[test]
493 fn a_horizontal_label_keeps_its_x_when_the_route_is_edited() {
494 let g = crate::grid::GRID_SIZE;
495 let geometry = single_edge_geometry(GridPoint { x: 0, y: 0 }, GridPoint { x: 10, y: 0 });
496 let (lid, labels) = label_at(&geometry, pos2(5.0 * g, 0.0));
497 let before = label_anchors(&labels, &geometry);
498 let old_x = before[0].1.x;
499
500 let geometry = single_edge_geometry(GridPoint { x: 0, y: 3 }, GridPoint { x: 10, y: 3 });
502 let after = reanchored(&geometry, &before);
503
504 assert_eq!(after[0].0, lid, "the same label was re-projected");
505 let loc = geometry.map_linear_distance_to_position(after[0].1);
506 assert!(
507 (loc.location.x - old_x).abs() < 0.5,
508 "a horizontal label keeps its X: {} -> {}",
509 old_x,
510 loc.location.x
511 );
512 assert!(
513 (loc.location.y - 3.0 * g).abs() < 0.5,
514 "its Y follows the moved wire"
515 );
516 }
517
518 #[test]
519 fn a_vertical_label_keeps_its_y_when_the_route_is_edited() {
520 let g = crate::grid::GRID_SIZE;
521 let geometry = single_edge_geometry(GridPoint { x: 0, y: 0 }, GridPoint { x: 0, y: 10 });
522 let (_, labels) = label_at(&geometry, pos2(0.0, 5.0 * g));
523 let before = label_anchors(&labels, &geometry);
524 let old_y = before[0].1.y;
525
526 let geometry = single_edge_geometry(GridPoint { x: 3, y: 0 }, GridPoint { x: 3, y: 10 });
528 let after = reanchored(&geometry, &before);
529
530 let loc = geometry.map_linear_distance_to_position(after[0].1);
531 assert!(
532 (loc.location.y - old_y).abs() < 0.5,
533 "a vertical label keeps its Y: {} -> {}",
534 old_y,
535 loc.location.y
536 );
537 assert!(
538 (loc.location.x - 3.0 * g).abs() < 0.5,
539 "its X follows the moved wire"
540 );
541 }
542
543 fn h(order: usize, y: i32, x_lo: i32, x_hi: i32) -> HEdge {
544 HEdge {
545 order,
546 y,
547 x_lo,
548 x_hi,
549 }
550 }
551 fn v(order: usize, x: i32, y_lo: i32, y_hi: i32) -> VEdge {
552 VEdge {
553 order,
554 x,
555 y_lo,
556 y_hi,
557 }
558 }
559
560 #[test]
561 fn later_route_hops_with_correct_orientation() {
562 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(1, 10, 0, 20)], 2);
564 assert!(out[0].is_empty(), "earlier route must not hop");
565 assert_eq!(
566 out[1],
567 vec![Crossing {
568 pos: GridPoint { x: 10, y: 10 },
569 orientation: RouteDirection::Vertical,
570 }],
571 "later (vertical) route hops over the earlier one"
572 );
573 }
574
575 #[test]
576 fn hopping_segment_orientation_follows_the_later_route() {
577 let out = compute_crossings(&[h(1, 10, 0, 20)], &[v(0, 10, 0, 20)], 2);
579 assert!(out[0].is_empty());
580 assert_eq!(
581 out[1],
582 vec![Crossing {
583 pos: GridPoint { x: 10, y: 10 },
584 orientation: RouteDirection::Horizontal,
585 }]
586 );
587 }
588
589 #[test]
590 fn multi_crossing_hops_every_vertical() {
591 let hop = |order| {
595 (
596 order,
597 vec![Crossing {
598 pos: GridPoint { x: 10, y: 10 },
599 orientation: RouteDirection::Vertical,
600 }],
601 )
602 };
603 let out = compute_crossings(&[h(1, 10, 0, 20)], &[v(0, 10, 0, 20), v(2, 10, 0, 20)], 3);
604 assert_eq!(out[hop(0).0], hop(0).1, "vertical route 0 hops");
605 assert_eq!(out[hop(2).0], hop(2).1, "vertical route 2 hops");
606 assert!(out[1].is_empty(), "the horizontal route is hopped over");
607 }
608
609 #[test]
610 fn t_junction_does_not_hop() {
611 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(1, 10, 10, 20)], 2);
614 assert!(out.iter().all(std::vec::Vec::is_empty));
615 }
616
617 #[test]
618 fn shared_corner_does_not_hop() {
619 let out = compute_crossings(&[h(0, 10, 0, 10)], &[v(1, 10, 10, 20)], 2);
621 assert!(out.iter().all(std::vec::Vec::is_empty));
622 }
623
624 #[test]
625 fn self_crossing_same_route_is_skipped() {
626 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(0, 10, 0, 20)], 1);
628 assert!(out[0].is_empty());
629 }
630}