1pub use crate::presentation::{Crossing, RouteGeometry};
2use blockworx_geom::{Pos2, Rect, vec2};
6use blockworx_paint::Renderer;
7use blockworx_router::{ClosedRouter, 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 materialize::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 crossings: Vec::new(),
320 }
321}
322
323pub fn reroute_preview_excluding(
332 waypoints: &[Waypoint],
333 geometry: &mut RouteGeometry,
334 ends: Endpoints,
335 excluded: &std::collections::HashSet<PathOrdinal>,
336 router: &mut ClosedRouter,
337) {
338 let mut unique_positions = std::collections::HashSet::new();
339 let mut wp_ids: Vec<PathOrdinal> = Vec::new();
340 let mut wp_positions: BTreeMap<PathOrdinal, Point> = BTreeMap::new();
341 for (index, wp) in waypoints.iter().enumerate() {
342 let id = PathOrdinal::new(index);
343 let pos = wp.pos;
344 if excluded.contains(&id)
345 || !router.is_accessible(Point::from(pos))
346 || !unique_positions.insert(pos)
347 {
348 continue;
349 }
350 wp_ids.push(id);
351 wp_positions.insert(id, Point::from(pos));
352 }
353 let path = RouteRequest {
354 start: ends.start.into(),
355 end: ends.end.into(),
356 wp_ids: &wp_ids,
357 wp_positions: &wp_positions,
358 self_cost: SelfCost::Apply,
359 }
360 .route(router);
361 *geometry = geometry_from_points(&path);
362 geometry.start_pos = ends.start;
363 geometry.end_pos = ends.end;
364 add_route_cost(
365 router,
366 geometry.edges.iter().map(|(_, edge)| edge),
367 WIRE_COST,
368 );
369}
370
371pub fn reroute_preview_closed(
378 waypoints: &[Waypoint],
379 geometry: &mut RouteGeometry,
380 ends: Endpoints,
381 waypoint_delta: GridVec,
382 router: &mut ClosedRouter,
383) {
384 let offset_positions: BTreeMap<PathOrdinal, Point> = waypoints
388 .iter()
389 .enumerate()
390 .map(|(index, wp)| {
391 (
392 PathOrdinal::new(index),
393 Point::from(wp.pos + waypoint_delta),
394 )
395 })
396 .collect();
397 let wp_ids: Vec<PathOrdinal> = (0..waypoints.len()).map(PathOrdinal::new).collect();
398 let path = RouteRequest {
399 start: ends.start.into(),
400 end: ends.end.into(),
401 wp_ids: &wp_ids,
402 wp_positions: &offset_positions,
403 self_cost: SelfCost::Apply,
404 }
405 .route(router);
406 let preview = geometry_from_points(&path);
407 add_route_cost(
408 router,
409 preview.edges.iter().map(|(_, edge)| edge),
410 WIRE_COST,
411 );
412 geometry.edges = preview.edges;
413 geometry.start_pos = ends.start;
414 geometry.end_pos = ends.end;
415}
416
417pub fn recompute_route_crossings(
427 scope_routes: &[RouteId],
428 geometries: &mut ahash::HashMap<RouteId, RouteGeometry>,
429) {
430 let mut h_edges: Vec<HEdge> = Vec::new();
431 let mut v_edges: Vec<VEdge> = Vec::new();
432 let mut num_routes = 0;
433 for (order, id) in scope_routes.iter().enumerate() {
434 num_routes = order + 1;
435 let Some(geometry) = geometries.get(id) else {
436 continue;
437 };
438 for (_, edge) in geometry.iter_edges() {
439 let (s, e) = (edge.start, edge.end);
440 if s.y == e.y && s.x != e.x {
441 h_edges.push(HEdge {
442 order,
443 y: s.y,
444 x_lo: s.x.min(e.x),
445 x_hi: s.x.max(e.x),
446 });
447 } else if s.x == e.x && s.y != e.y {
448 v_edges.push(VEdge {
449 order,
450 x: s.x,
451 y_lo: s.y.min(e.y),
452 y_hi: s.y.max(e.y),
453 });
454 }
455 }
456 }
457 let mut per_route = compute_crossings(&h_edges, &v_edges, num_routes);
458 for (order, id) in scope_routes.iter().enumerate() {
459 if let Some(geometry) = geometries.get_mut(id) {
460 geometry.crossings = std::mem::take(&mut per_route[order]);
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::{
468 Crossing, HEdge, RouteGeometry, VEdge, compute_crossings, label_anchors, reanchored,
469 };
470 use crate::presentation::{
471 RouteDirection, RouteEdge,
472 store::{EdgeId, IdMap, IdMapExt},
473 };
474 use blockworx_doc::{
475 fixtures::route_label_id,
476 geometry::{FracVal, GridPoint},
477 id::RouteLabelId,
478 };
479 use blockworx_geom::{Pos2, pos2};
480
481 fn single_edge_geometry(start: GridPoint, end: GridPoint) -> RouteGeometry {
484 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
485 edges.insert_value(RouteEdge { start, end });
486 RouteGeometry {
487 edges,
488 start_pos: start,
489 end_pos: end,
490 crossings: Vec::new(),
491 }
492 }
493
494 fn label_at(
497 geometry: &RouteGeometry,
498 pos: Pos2,
499 ) -> (RouteLabelId, Vec<(RouteLabelId, FracVal)>) {
500 let lid = route_label_id(1);
501 (lid, vec![(lid, geometry.distance_along(pos))])
502 }
503
504 #[test]
505 fn a_horizontal_label_keeps_its_x_when_the_route_is_edited() {
506 let g = crate::grid::GRID_SIZE;
507 let geometry = single_edge_geometry(GridPoint { x: 0, y: 0 }, GridPoint { x: 10, y: 0 });
508 let (lid, labels) = label_at(&geometry, pos2(5.0 * g, 0.0));
509 let before = label_anchors(&labels, &geometry);
510 let old_x = before[0].1.x;
511
512 let geometry = single_edge_geometry(GridPoint { x: 0, y: 3 }, GridPoint { x: 10, y: 3 });
514 let after = reanchored(&geometry, &before);
515
516 assert_eq!(after[0].0, lid, "the same label was re-projected");
517 let loc = geometry.map_linear_distance_to_position(after[0].1);
518 assert!(
519 (loc.location.x - old_x).abs() < 0.5,
520 "a horizontal label keeps its X: {} -> {}",
521 old_x,
522 loc.location.x
523 );
524 assert!(
525 (loc.location.y - 3.0 * g).abs() < 0.5,
526 "its Y follows the moved wire"
527 );
528 }
529
530 #[test]
531 fn a_vertical_label_keeps_its_y_when_the_route_is_edited() {
532 let g = crate::grid::GRID_SIZE;
533 let geometry = single_edge_geometry(GridPoint { x: 0, y: 0 }, GridPoint { x: 0, y: 10 });
534 let (_, labels) = label_at(&geometry, pos2(0.0, 5.0 * g));
535 let before = label_anchors(&labels, &geometry);
536 let old_y = before[0].1.y;
537
538 let geometry = single_edge_geometry(GridPoint { x: 3, y: 0 }, GridPoint { x: 3, y: 10 });
540 let after = reanchored(&geometry, &before);
541
542 let loc = geometry.map_linear_distance_to_position(after[0].1);
543 assert!(
544 (loc.location.y - old_y).abs() < 0.5,
545 "a vertical label keeps its Y: {} -> {}",
546 old_y,
547 loc.location.y
548 );
549 assert!(
550 (loc.location.x - 3.0 * g).abs() < 0.5,
551 "its X follows the moved wire"
552 );
553 }
554
555 fn h(order: usize, y: i32, x_lo: i32, x_hi: i32) -> HEdge {
556 HEdge {
557 order,
558 y,
559 x_lo,
560 x_hi,
561 }
562 }
563 fn v(order: usize, x: i32, y_lo: i32, y_hi: i32) -> VEdge {
564 VEdge {
565 order,
566 x,
567 y_lo,
568 y_hi,
569 }
570 }
571
572 #[test]
573 fn later_route_hops_with_correct_orientation() {
574 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(1, 10, 0, 20)], 2);
576 assert!(out[0].is_empty(), "earlier route must not hop");
577 assert_eq!(
578 out[1],
579 vec![Crossing {
580 pos: GridPoint { x: 10, y: 10 },
581 orientation: RouteDirection::Vertical,
582 }],
583 "later (vertical) route hops over the earlier one"
584 );
585 }
586
587 #[test]
588 fn hopping_segment_orientation_follows_the_later_route() {
589 let out = compute_crossings(&[h(1, 10, 0, 20)], &[v(0, 10, 0, 20)], 2);
591 assert!(out[0].is_empty());
592 assert_eq!(
593 out[1],
594 vec![Crossing {
595 pos: GridPoint { x: 10, y: 10 },
596 orientation: RouteDirection::Horizontal,
597 }]
598 );
599 }
600
601 #[test]
602 fn multi_crossing_hops_every_vertical() {
603 let hop = |order| {
607 (
608 order,
609 vec![Crossing {
610 pos: GridPoint { x: 10, y: 10 },
611 orientation: RouteDirection::Vertical,
612 }],
613 )
614 };
615 let out = compute_crossings(&[h(1, 10, 0, 20)], &[v(0, 10, 0, 20), v(2, 10, 0, 20)], 3);
616 assert_eq!(out[hop(0).0], hop(0).1, "vertical route 0 hops");
617 assert_eq!(out[hop(2).0], hop(2).1, "vertical route 2 hops");
618 assert!(out[1].is_empty(), "the horizontal route is hopped over");
619 }
620
621 #[test]
622 fn t_junction_does_not_hop() {
623 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(1, 10, 10, 20)], 2);
626 assert!(out.iter().all(std::vec::Vec::is_empty));
627 }
628
629 #[test]
630 fn shared_corner_does_not_hop() {
631 let out = compute_crossings(&[h(0, 10, 0, 10)], &[v(1, 10, 10, 20)], 2);
633 assert!(out.iter().all(std::vec::Vec::is_empty));
634 }
635
636 #[test]
637 fn self_crossing_same_route_is_skipped() {
638 let out = compute_crossings(&[h(0, 10, 0, 20)], &[v(0, 10, 0, 20)], 1);
640 assert!(out[0].is_empty());
641 }
642}