1use std::collections::{BTreeMap, HashSet};
11
12use blockworx_doc::geometry::{GridPoint, Waypoint};
13use blockworx_router::block::{Block as ObstacleRect, ROUTE_GUTTER};
14use blockworx_router::{ClosedRouter, Direction, Resolution, WIRE_COST, direction_between};
15
16use crate::presentation::RouteEdge;
17use crate::presentation::RouteGeometry;
18use crate::presentation::store::{EdgeId, IdMap, IdMapExt};
19use crate::widget::waypoint_router::add_route_cost;
20
21fn corner_positions(waypoints: &[Waypoint]) -> Vec<GridPoint> {
25 waypoints.iter().map(|wp| wp.pos).collect()
26}
27
28pub struct Obstacles {
39 rects: Vec<ObstacleRect>,
40 by_row: BTreeMap<i32, Vec<usize>>,
41 by_col: BTreeMap<i32, Vec<usize>>,
42}
43
44impl Obstacles {
45 pub fn new(rects: Vec<ObstacleRect>) -> Self {
46 let mut by_row: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
47 let mut by_col: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
48 for (i, r) in rects.iter().enumerate() {
49 for y in r.top_left.y.raw()..=r.bottom_right.y.raw() {
50 by_row.entry(y).or_default().push(i);
51 }
52 for x in r.top_left.x.raw()..=r.bottom_right.x.raw() {
53 by_col.entry(x).or_default().push(i);
54 }
55 }
56 Self {
57 rects,
58 by_row,
59 by_col,
60 }
61 }
62
63 pub fn blocked(&self, a: GridPoint, b: GridPoint) -> bool {
67 self.crossings(a, b).next().is_some()
68 }
69
70 pub fn crossings(
73 &self,
74 a: GridPoint,
75 b: GridPoint,
76 ) -> impl Iterator<Item = (GridPoint, GridPoint)> + '_ {
77 let candidates = if a.y == b.y {
78 self.by_row.get(&a.y)
79 } else if a.x == b.x {
80 self.by_col.get(&a.x)
81 } else {
82 None
83 };
84 candidates
85 .into_iter()
86 .flatten()
87 .filter_map(move |&i| self.rects[i].edge_crossing(a, b))
88 .map(|(from, to)| (GridPoint::from(from), GridPoint::from(to)))
89 }
90
91 pub fn hugs(&self, a: GridPoint, b: GridPoint) -> bool {
100 let (index, at) = if a.y == b.y {
101 (&self.by_row, a.y)
102 } else if a.x == b.x {
103 (&self.by_col, a.x)
104 } else {
105 return false;
106 };
107 index
108 .range(at - ROUTE_GUTTER..=at + ROUTE_GUTTER)
109 .flat_map(|(_, bucket)| bucket)
110 .any(|&i| self.rects[i].hugs_wire(a, b, ROUTE_GUTTER))
111 }
112
113 pub fn accessible(&self, p: GridPoint) -> bool {
115 !self
116 .by_row
117 .get(&p.y)
118 .into_iter()
119 .flatten()
120 .any(|&i| self.rects[i].contains(p.into()))
121 }
122}
123
124fn axis_aligned(a: GridPoint, b: GridPoint) -> bool {
125 a.x == b.x || a.y == b.y
126}
127
128#[derive(Clone, Copy)]
131pub struct Endpoints {
132 pub start: GridPoint,
133 pub end: GridPoint,
134}
135
136fn reconstruct_legs(
149 stored: &[Waypoint],
150 geometry: &mut RouteGeometry,
151 ends: Endpoints,
152 rules: LegRules<'_>,
153 mut router: Option<&mut ClosedRouter>,
154) -> Reconstruction {
155 let LegRules { obstacles, blocked } = rules;
156 let mut corners: Vec<GridPoint> = Vec::with_capacity(stored.len() + 2);
159 corners.push(ends.start);
160 for pos in corner_positions(stored) {
161 if blocked == BlockedLeg::Reroute && router.is_some() && !obstacles.accessible(pos) {
162 continue;
163 }
164 corners.push(pos);
165 }
166 corners.push(ends.end);
167
168 let mut polyline: Vec<GridPoint> = vec![corners[0]];
171 let mut incoming: Option<Direction> = None;
172 let mut resolution = Resolution::Routed;
173 for pair in corners.windows(2) {
174 let (a, b) = (pair[0], pair[1]);
175 if a == b {
176 continue;
177 }
178 let clear = blocked == BlockedLeg::Keep || !obstacles.blocked(a, b);
179 if axis_aligned(a, b) && clear && !obstacles.hugs(a, b) {
180 polyline.push(b);
181 incoming = direction_between(a.into(), b.into());
182 continue;
183 }
184 let Some(r) = router.as_mut() else {
185 return Reconstruction::NeedsLattice;
186 };
187 let leg = r.route_leg(a.into(), b.into(), incoming);
188 if leg.path.len() >= 2 {
189 polyline.extend(leg.path[1..].iter().map(|&p| GridPoint::from(p)));
190 } else {
191 polyline.push(b);
192 }
193 incoming = leg.outgoing;
194 resolution = resolution.and(leg.resolution);
195 }
196
197 let locked = locked_corners(stored);
198 let path = corners_of(&polyline);
199 let promoted = write_geometry(geometry, ends, &path, &locked);
200
201 if let Some(r) = router.as_mut() {
202 add_route_cost(r, geometry.iter_edges().map(|(_, e)| e), WIRE_COST);
203 }
204 Reconstruction::Laid(Laid {
205 corners: promoted,
206 resolution,
207 })
208}
209
210#[derive(Clone, Copy)]
213pub struct LegRules<'a> {
214 obstacles: &'a Obstacles,
215 blocked: BlockedLeg,
216}
217
218impl Obstacles {
219 #[must_use]
221 pub fn reroute_blocked(&self) -> LegRules<'_> {
222 LegRules {
223 obstacles: self,
224 blocked: BlockedLeg::Reroute,
225 }
226 }
227
228 #[must_use]
231 pub fn keep_blocked(&self) -> LegRules<'_> {
232 LegRules {
233 obstacles: self,
234 blocked: BlockedLeg::Keep,
235 }
236 }
237}
238
239#[derive(Clone, Copy, PartialEq, Eq, Debug)]
241enum BlockedLeg {
242 Reroute,
245 Keep,
249}
250
251pub struct Laid {
255 pub corners: Vec<Waypoint>,
256 pub resolution: Resolution,
257}
258
259pub fn straighten_route(
264 stored: &[Waypoint],
265 geometry: &mut RouteGeometry,
266 ends: Endpoints,
267 rules: LegRules<'_>,
268) -> Option<Vec<Waypoint>> {
269 match reconstruct_legs(stored, geometry, ends, rules, None) {
270 Reconstruction::Laid(laid) => Some(laid.corners),
271 Reconstruction::NeedsLattice => None,
272 }
273}
274
275pub fn reconstruct_route(
279 stored: &[Waypoint],
280 geometry: &mut RouteGeometry,
281 ends: Endpoints,
282 rules: LegRules<'_>,
283 lattice: &mut ClosedRouter,
284) -> Laid {
285 match reconstruct_legs(stored, geometry, ends, rules, Some(lattice)) {
286 Reconstruction::Laid(laid) => laid,
287 Reconstruction::NeedsLattice => unreachable!("a lattice was given"),
288 }
289}
290
291enum Reconstruction {
293 Laid(Laid),
294 NeedsLattice,
295}
296
297pub fn reconstruct_corners_direct(
314 waypoints: &[GridPoint],
315 geometry: &mut RouteGeometry,
316 ends: Endpoints,
317) {
318 let mut corners: Vec<GridPoint> = Vec::with_capacity(waypoints.len() + 2);
319 corners.push(ends.start);
320 corners.extend_from_slice(waypoints);
321 corners.push(ends.end);
322
323 let last_leg = corners.len().saturating_sub(2);
324 let has_waypoints = corners.len() > 2;
325 let mut polyline: Vec<GridPoint> = vec![corners[0]];
326 let mut incoming: Option<Direction> = None;
327 for (i, pair) in corners.windows(2).enumerate() {
328 let (a, b) = (pair[0], pair[1]);
329 if a == b {
330 continue;
331 }
332 if axis_aligned(a, b) {
333 polyline.push(b);
334 } else {
335 let horizontal_first = if has_waypoints && i == 0 {
336 true } else if has_waypoints && i == last_leg {
338 false } else {
340 !matches!(incoming, Some(Direction::North | Direction::South))
341 };
342 let bend = if horizontal_first {
343 GridPoint { x: b.x, y: a.y }
344 } else {
345 GridPoint { x: a.x, y: b.y }
346 };
347 polyline.push(bend);
348 polyline.push(b);
349 }
350 incoming = direction_between(polyline[polyline.len() - 2].into(), b.into());
351 }
352
353 let path = corners_of(&polyline);
354 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
355 for w in path.windows(2) {
356 edges.insert_value(RouteEdge {
357 start: w[0],
358 end: w[1],
359 });
360 }
361 geometry.edges = edges;
362 geometry.start_pos = ends.start;
363 geometry.end_pos = ends.end;
364}
365
366fn write_geometry(
372 geometry: &mut RouteGeometry,
373 ends: Endpoints,
374 path: &[GridPoint],
375 locked: &HashSet<GridPoint>,
376) -> Vec<Waypoint> {
377 geometry.edges = edges_of(path);
378 geometry.start_pos = ends.start;
379 geometry.end_pos = ends.end;
380 interior_waypoints(path, locked)
381}
382
383fn edges_of(path: &[GridPoint]) -> IdMap<EdgeId, RouteEdge> {
385 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
386 for w in path.windows(2) {
387 edges.insert_value(RouteEdge {
388 start: w[0],
389 end: w[1],
390 });
391 }
392 edges
393}
394
395fn interior_waypoints(path: &[GridPoint], locked: &HashSet<GridPoint>) -> Vec<Waypoint> {
398 if path.len() <= 2 {
399 return Vec::new();
400 }
401 path[1..path.len() - 1]
402 .iter()
403 .map(|&pos| Waypoint {
404 pos,
405 locked: locked.contains(&pos),
406 })
407 .collect()
408}
409
410fn locked_corners(waypoints: &[Waypoint]) -> HashSet<GridPoint> {
412 waypoints
413 .iter()
414 .filter(|wp| wp.locked)
415 .map(|wp| wp.pos)
416 .collect()
417}
418
419pub fn corners_of(points: &[GridPoint]) -> Vec<GridPoint> {
424 let mut out: Vec<GridPoint> = Vec::with_capacity(points.len());
425 for &p in points {
426 if out.last() == Some(&p) {
427 continue;
428 }
429 if let [.., a, b] = out[..]
430 && collinear(a, b, p)
431 {
432 out.pop();
433 }
434 out.push(p);
435 }
436 out
437}
438
439pub fn promote_corners_to_waypoints(
447 stored: &[Waypoint],
448 geometry: &mut RouteGeometry,
449) -> Vec<Waypoint> {
450 let corners = geometry_corners(geometry);
451 let locked = locked_corners(stored);
452
453 geometry.edges = edges_of(&corners);
462 interior_waypoints(&corners, &locked)
463}
464
465pub fn geometry_corners(geometry: &RouteGeometry) -> Vec<GridPoint> {
467 let mut polyline = vec![geometry.start_pos];
468 polyline.extend(geometry.iter_edges().map(|(_, e)| e.end));
469 polyline.push(geometry.end_pos);
470 corners_of(&polyline)
471}
472
473fn collinear(a: GridPoint, b: GridPoint, c: GridPoint) -> bool {
477 (b.x - a.x) as i64 * (c.y - a.y) as i64 == (b.y - a.y) as i64 * (c.x - a.x) as i64
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 use crate::edit::geometry::{backtracking_ordinals, pruned_waypoints};
485
486 fn g(x: i32, y: i32) -> GridPoint {
487 GridPoint { x, y }
488 }
489
490 fn wp(x: i32, y: i32) -> Waypoint {
491 Waypoint {
492 pos: GridPoint { x, y },
493 locked: false,
494 }
495 }
496
497 fn loaded_route(waypoints: &[(i32, i32)]) -> Vec<Waypoint> {
500 waypoints.iter().map(|&(x, y)| wp(x, y)).collect()
501 }
502
503 fn edges_double_back(geometry: &RouteGeometry) -> bool {
504 let edges: Vec<_> = geometry
505 .iter_edges()
506 .map(|(_, e)| (e.start, e.end))
507 .collect();
508 edges.windows(2).any(|w| {
509 let (ax, ay) = (w[0].1.x - w[0].0.x, w[0].1.y - w[0].0.y);
510 let (bx, by) = (w[1].1.x - w[1].0.x, w[1].1.y - w[1].0.y);
511 (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
512 || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
513 })
514 }
515
516 #[test]
517 fn promote_erases_a_backtracking_spike_from_the_edges() {
518 use crate::edit::create::PathOrdinal;
519 use crate::widget::segmentkind::SegmentKind;
520 use crate::widget::waypoint_router::TaggedPoint;
521 use blockworx_router::point::Point;
522
523 let tp = |segment, x, y| TaggedPoint {
524 segment,
525 pos: Point::from(g(x, y)),
526 };
527 let w = PathOrdinal::new(0);
531 let path = [
532 tp(SegmentKind::StartToWaypoint(w), 0, 0),
533 tp(SegmentKind::StartToWaypoint(w), 10, 0),
534 tp(SegmentKind::WaypointToEnd(w), 10, 0),
535 tp(SegmentKind::WaypointToEnd(w), 5, 0),
536 ];
537 let stored = loaded_route(&[(10, 0)]);
538 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
539 assert!(
540 edges_double_back(&geometry),
541 "setup: the solve kept the reversal spike in the edges"
542 );
543
544 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
545 assert!(
546 !edges_double_back(&geometry),
547 "promote collapsed the spike in the edges"
548 );
549 assert!(promoted.is_empty(), "and dropped the overshoot waypoint");
550 }
551
552 #[test]
553 fn corners_of_drops_collinear_interior_points() {
554 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 1), g(2, 2)];
556 assert_eq!(corners_of(&pts), vec![g(0, 0), g(2, 0), g(2, 2)]);
557 }
558
559 #[test]
560 fn corners_of_drops_adjacent_duplicates() {
561 let pts = [g(0, 0), g(0, 0), g(3, 0), g(3, 0)];
562 assert_eq!(corners_of(&pts), vec![g(0, 0), g(3, 0)]);
563 }
564
565 #[test]
566 fn corners_of_is_idempotent() {
567 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 3), g(5, 3), g(5, 3)];
568 let once = corners_of(&pts);
569 assert_eq!(corners_of(&once), once);
570 }
571
572 #[test]
573 fn promote_makes_every_bend_a_waypoint() {
574 use crate::edit::create::PathOrdinal;
575 use crate::widget::segmentkind::SegmentKind;
576 use crate::widget::waypoint_router::TaggedPoint;
577 use blockworx_router::point::Point;
578
579 let tp = |x: i32, y: i32| TaggedPoint {
580 segment: SegmentKind::StartToEnd,
581 pos: Point::from(GridPoint { x, y }),
582 };
583 let path = [tp(0, 0), tp(3, 0), tp(3, 3), tp(6, 3)];
585 let stored = loaded_route(&[]);
586 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
587 assert!(stored.is_empty());
588 assert_ne!(
589 PathOrdinal::new(0),
590 PathOrdinal::new(1),
591 "ordinals name distinct corners"
592 );
593
594 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
595
596 assert_eq!(corner_positions(&promoted), vec![g(3, 0), g(3, 3)]);
597 assert!(promoted.iter().all(|wp| !wp.locked));
598 }
599
600 #[test]
601 fn straight_legs_reconstruct_without_a_router() {
602 let stored = loaded_route(&[(3, 0)]);
603 let mut geometry = RouteGeometry::default();
604 let done = straighten_route(
606 &stored,
607 &mut geometry,
608 Endpoints {
609 start: g(0, 0),
610 end: g(3, 4),
611 },
612 Obstacles::new(vec![]).reroute_blocked(),
613 );
614 assert!(done.is_some(), "a straight route needs no router");
615 let edges: Vec<(GridPoint, GridPoint)> = geometry
616 .iter_edges()
617 .map(|(_, e)| (e.start, e.end))
618 .collect();
619 assert_eq!(edges, vec![(g(0, 0), g(3, 0)), (g(3, 0), g(3, 4))]);
620 }
621
622 #[test]
623 fn a_blocked_leg_defers_when_no_router() {
624 let stored = loaded_route(&[]);
625 let mut geometry = RouteGeometry::default();
626 let obstacles = Obstacles::new(vec![ObstacleRect {
628 top_left: g(4, -2).into(),
629 bottom_right: g(6, 2).into(),
630 }]);
631 let done = straighten_route(
632 &stored,
633 &mut geometry,
634 Endpoints {
635 start: g(0, 0),
636 end: g(10, 0),
637 },
638 obstacles.reroute_blocked(),
639 );
640 assert!(
641 done.is_none(),
642 "a blocked straight leg must defer to the router"
643 );
644 }
645
646 #[test]
647 fn a_blocked_leg_is_kept_as_it_stands_when_the_document_says_so() {
648 let stored = loaded_route(&[]);
649 let mut geometry = RouteGeometry::default();
650 let obstacles = Obstacles::new(vec![ObstacleRect {
651 top_left: g(4, -2).into(),
652 bottom_right: g(6, 2).into(),
653 }]);
654 assert!(
655 obstacles.blocked(g(0, 0), g(10, 0)),
656 "precondition: the leg runs through the block"
657 );
658 let done = straighten_route(
659 &stored,
660 &mut geometry,
661 Endpoints {
662 start: g(0, 0),
663 end: g(10, 0),
664 },
665 obstacles.keep_blocked(),
666 );
667 assert!(done.is_some(), "a kept leg needs no router");
668 let edges: Vec<(GridPoint, GridPoint)> = geometry
669 .iter_edges()
670 .map(|(_, e)| (e.start, e.end))
671 .collect();
672 assert_eq!(edges, vec![(g(0, 0), g(10, 0))]);
673 assert_eq!(
674 obstacles.crossings(g(0, 0), g(10, 0)).collect::<Vec<_>>(),
675 vec![(g(4, 0), g(6, 0))],
676 "the crossing is the stretch inside the block"
677 );
678 }
679
680 #[test]
681 fn direct_relay_draws_straight_legs_and_leaves_its_corners() {
682 let corners = [g(0, 5)];
683 let mut geometry = RouteGeometry::default();
684 reconstruct_corners_direct(
685 &corners,
686 &mut geometry,
687 Endpoints {
688 start: g(0, 0),
689 end: g(5, 5),
690 },
691 );
692 let edges: Vec<(GridPoint, GridPoint)> = geometry
693 .iter_edges()
694 .map(|(_, e)| (e.start, e.end))
695 .collect();
696 assert_eq!(edges, vec![(g(0, 0), g(0, 5)), (g(0, 5), g(5, 5))]);
697 assert_eq!(corners, [g(0, 5)]);
700 }
701
702 #[test]
703 fn direct_relay_l_bends_a_diagonal_leg_without_a_router() {
704 let mut geometry = RouteGeometry::default();
705 reconstruct_corners_direct(
706 &[g(3, 4)],
707 &mut geometry,
708 Endpoints {
709 start: g(0, 0),
710 end: g(3, 8),
711 },
712 );
713 assert!(
714 geometry
715 .iter_edges()
716 .all(|(_, e)| e.start.x == e.end.x || e.start.y == e.end.y),
717 "every relayed edge is axis-aligned — an L, never a diagonal"
718 );
719 assert!(
720 geometry.iter_edges().count() >= 2,
721 "the diagonal leg produced a bend"
722 );
723 }
724
725 #[test]
726 fn a_non_colinear_leg_defers_when_no_router() {
727 let stored = loaded_route(&[(3, 4)]);
729 let mut geometry = RouteGeometry::default();
730 let done = straighten_route(
731 &stored,
732 &mut geometry,
733 Endpoints {
734 start: g(0, 0),
735 end: g(3, 8),
736 },
737 Obstacles::new(vec![]).reroute_blocked(),
738 );
739 assert!(
740 done.is_none(),
741 "a non-colinear leg must defer to the router"
742 );
743 }
744
745 fn pruned(
749 waypoints: &[(i32, i32)],
750 start: GridPoint,
751 end: GridPoint,
752 ) -> (Vec<GridPoint>, bool) {
753 let stored: Vec<Waypoint> = waypoints.iter().map(|&(x, y)| wp(x, y)).collect();
754 let doomed = backtracking_ordinals(&stored, start, end);
755 (
756 corner_positions(&pruned_waypoints(&stored, &doomed)),
757 !doomed.is_empty(),
758 )
759 }
760
761 #[test]
762 fn removes_a_waypoint_the_wire_doubles_back_on() {
763 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(5, 0));
766 assert!(removed);
767 assert!(kept.is_empty());
768 }
769
770 #[test]
771 fn keeps_a_real_corner_and_a_straight_through_pin() {
772 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(10, 5));
774 assert!(!removed);
775 assert_eq!(kept, vec![g(10, 0)]);
776 let (kept, removed) = pruned(&[(5, 0)], g(0, 0), g(10, 0));
779 assert!(!removed);
780 assert_eq!(kept.len(), 1);
781 }
782
783 #[test]
784 fn cascades_after_a_removal_exposes_a_new_reversal() {
785 let (kept, removed) = pruned(&[(10, 0), (3, 0)], g(0, 0), g(5, 0));
788 assert!(removed);
789 assert_eq!(kept, vec![g(3, 0)]);
790 }
791}