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, 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 let candidates = if a.y == b.y {
68 self.by_row.get(&a.y)
69 } else if a.x == b.x {
70 self.by_col.get(&a.x)
71 } else {
72 None
73 };
74 candidates
75 .into_iter()
76 .flatten()
77 .any(|&i| self.rects[i].intersects_edge(a, b))
78 }
79
80 pub fn hugs(&self, a: GridPoint, b: GridPoint) -> bool {
89 let (index, at) = if a.y == b.y {
90 (&self.by_row, a.y)
91 } else if a.x == b.x {
92 (&self.by_col, a.x)
93 } else {
94 return false;
95 };
96 index
97 .range(at - ROUTE_GUTTER..=at + ROUTE_GUTTER)
98 .flat_map(|(_, bucket)| bucket)
99 .any(|&i| self.rects[i].hugs_wire(a, b, ROUTE_GUTTER))
100 }
101
102 pub fn accessible(&self, p: GridPoint) -> bool {
104 !self
105 .by_row
106 .get(&p.y)
107 .into_iter()
108 .flatten()
109 .any(|&i| self.rects[i].contains(p.into()))
110 }
111}
112
113fn axis_aligned(a: GridPoint, b: GridPoint) -> bool {
114 a.x == b.x || a.y == b.y
115}
116
117#[derive(Clone, Copy)]
120pub struct Endpoints {
121 pub start: GridPoint,
122 pub end: GridPoint,
123}
124
125pub fn materialize_route(
143 stored: &[Waypoint],
144 geometry: &mut RouteGeometry,
145 ends: Endpoints,
146 obstacles: &Obstacles,
147 mut router: Option<&mut ClosedRouter>,
148) -> Option<Vec<Waypoint>> {
149 let mut corners: Vec<GridPoint> = Vec::with_capacity(stored.len() + 2);
152 corners.push(ends.start);
153 for pos in corner_positions(stored) {
154 if router.is_some() && !obstacles.accessible(pos) {
155 continue;
156 }
157 corners.push(pos);
158 }
159 corners.push(ends.end);
160
161 let mut polyline: Vec<GridPoint> = vec![corners[0]];
164 let mut incoming: Option<Direction> = None;
165 for pair in corners.windows(2) {
166 let (a, b) = (pair[0], pair[1]);
167 if a == b {
168 continue;
169 }
170 if axis_aligned(a, b) && !obstacles.blocked(a, b) && !obstacles.hugs(a, b) {
171 polyline.push(b);
172 incoming = direction_between(a.into(), b.into());
173 continue;
174 }
175 let r = router.as_mut()?;
176 let (subpath, outgoing) = r.route_leg(a.into(), b.into(), incoming);
177 if subpath.len() >= 2 {
178 polyline.extend(subpath[1..].iter().map(|&p| GridPoint::from(p)));
179 } else {
180 polyline.push(b);
181 }
182 incoming = outgoing;
183 }
184
185 let locked = locked_corners(stored);
186 let path = corners_of(&polyline);
187 let promoted = write_geometry(geometry, ends, &path, &locked);
188
189 if let Some(r) = router.as_mut() {
190 add_route_cost(r, geometry.iter_edges().map(|(_, e)| e), WIRE_COST);
191 }
192 Some(promoted)
193}
194
195pub fn materialize_corners_direct(
212 waypoints: &[GridPoint],
213 geometry: &mut RouteGeometry,
214 ends: Endpoints,
215) {
216 let mut corners: Vec<GridPoint> = Vec::with_capacity(waypoints.len() + 2);
217 corners.push(ends.start);
218 corners.extend_from_slice(waypoints);
219 corners.push(ends.end);
220
221 let last_leg = corners.len().saturating_sub(2);
222 let has_waypoints = corners.len() > 2;
223 let mut polyline: Vec<GridPoint> = vec![corners[0]];
224 let mut incoming: Option<Direction> = None;
225 for (i, pair) in corners.windows(2).enumerate() {
226 let (a, b) = (pair[0], pair[1]);
227 if a == b {
228 continue;
229 }
230 if axis_aligned(a, b) {
231 polyline.push(b);
232 } else {
233 let horizontal_first = if has_waypoints && i == 0 {
234 true } else if has_waypoints && i == last_leg {
236 false } else {
238 !matches!(incoming, Some(Direction::North | Direction::South))
239 };
240 let bend = if horizontal_first {
241 GridPoint { x: b.x, y: a.y }
242 } else {
243 GridPoint { x: a.x, y: b.y }
244 };
245 polyline.push(bend);
246 polyline.push(b);
247 }
248 incoming = direction_between(polyline[polyline.len() - 2].into(), b.into());
249 }
250
251 let path = corners_of(&polyline);
252 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
253 for w in path.windows(2) {
254 edges.insert_value(RouteEdge {
255 start: w[0],
256 end: w[1],
257 });
258 }
259 geometry.edges = edges;
260 geometry.start_pos = ends.start;
261 geometry.end_pos = ends.end;
262}
263
264fn write_geometry(
270 geometry: &mut RouteGeometry,
271 ends: Endpoints,
272 path: &[GridPoint],
273 locked: &HashSet<GridPoint>,
274) -> Vec<Waypoint> {
275 geometry.edges = edges_of(path);
276 geometry.start_pos = ends.start;
277 geometry.end_pos = ends.end;
278 interior_waypoints(path, locked)
279}
280
281fn edges_of(path: &[GridPoint]) -> IdMap<EdgeId, RouteEdge> {
283 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
284 for w in path.windows(2) {
285 edges.insert_value(RouteEdge {
286 start: w[0],
287 end: w[1],
288 });
289 }
290 edges
291}
292
293fn interior_waypoints(path: &[GridPoint], locked: &HashSet<GridPoint>) -> Vec<Waypoint> {
296 if path.len() <= 2 {
297 return Vec::new();
298 }
299 path[1..path.len() - 1]
300 .iter()
301 .map(|&pos| Waypoint {
302 pos,
303 locked: locked.contains(&pos),
304 })
305 .collect()
306}
307
308fn locked_corners(waypoints: &[Waypoint]) -> HashSet<GridPoint> {
310 waypoints
311 .iter()
312 .filter(|wp| wp.locked)
313 .map(|wp| wp.pos)
314 .collect()
315}
316
317pub fn corners_of(points: &[GridPoint]) -> Vec<GridPoint> {
322 let mut out: Vec<GridPoint> = Vec::with_capacity(points.len());
323 for &p in points {
324 if out.last() == Some(&p) {
325 continue;
326 }
327 if let [.., a, b] = out[..]
328 && collinear(a, b, p)
329 {
330 out.pop();
331 }
332 out.push(p);
333 }
334 out
335}
336
337pub fn promote_corners_to_waypoints(
345 stored: &[Waypoint],
346 geometry: &mut RouteGeometry,
347) -> Vec<Waypoint> {
348 let corners = geometry_corners(geometry);
349 let locked = locked_corners(stored);
350
351 geometry.edges = edges_of(&corners);
360 interior_waypoints(&corners, &locked)
361}
362
363pub fn geometry_corners(geometry: &RouteGeometry) -> Vec<GridPoint> {
365 let mut polyline = vec![geometry.start_pos];
366 polyline.extend(geometry.iter_edges().map(|(_, e)| e.end));
367 polyline.push(geometry.end_pos);
368 corners_of(&polyline)
369}
370
371fn collinear(a: GridPoint, b: GridPoint, c: GridPoint) -> bool {
375 (b.x - a.x) as i64 * (c.y - a.y) as i64 == (b.y - a.y) as i64 * (c.x - a.x) as i64
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 use crate::edit::geometry::{backtracking_ordinals, pruned_waypoints};
383
384 fn g(x: i32, y: i32) -> GridPoint {
385 GridPoint { x, y }
386 }
387
388 fn wp(x: i32, y: i32) -> Waypoint {
389 Waypoint {
390 pos: GridPoint { x, y },
391 locked: false,
392 }
393 }
394
395 fn loaded_route(waypoints: &[(i32, i32)]) -> Vec<Waypoint> {
398 waypoints.iter().map(|&(x, y)| wp(x, y)).collect()
399 }
400
401 fn edges_double_back(geometry: &RouteGeometry) -> bool {
402 let edges: Vec<_> = geometry
403 .iter_edges()
404 .map(|(_, e)| (e.start, e.end))
405 .collect();
406 edges.windows(2).any(|w| {
407 let (ax, ay) = (w[0].1.x - w[0].0.x, w[0].1.y - w[0].0.y);
408 let (bx, by) = (w[1].1.x - w[1].0.x, w[1].1.y - w[1].0.y);
409 (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
410 || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
411 })
412 }
413
414 #[test]
415 fn promote_erases_a_backtracking_spike_from_the_edges() {
416 use crate::edit::create::PathOrdinal;
417 use crate::widget::segmentkind::SegmentKind;
418 use crate::widget::waypoint_router::TaggedPoint;
419 use blockworx_router::point::Point;
420
421 let tp = |segment, x, y| TaggedPoint {
422 segment,
423 pos: Point::from(g(x, y)),
424 };
425 let w = PathOrdinal::new(0);
429 let path = [
430 tp(SegmentKind::StartToWaypoint(w), 0, 0),
431 tp(SegmentKind::StartToWaypoint(w), 10, 0),
432 tp(SegmentKind::WaypointToEnd(w), 10, 0),
433 tp(SegmentKind::WaypointToEnd(w), 5, 0),
434 ];
435 let stored = loaded_route(&[(10, 0)]);
436 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
437 assert!(
438 edges_double_back(&geometry),
439 "setup: the solve kept the reversal spike in the edges"
440 );
441
442 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
443 assert!(
444 !edges_double_back(&geometry),
445 "promote collapsed the spike in the edges"
446 );
447 assert!(promoted.is_empty(), "and dropped the overshoot waypoint");
448 }
449
450 #[test]
451 fn corners_of_drops_collinear_interior_points() {
452 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 1), g(2, 2)];
454 assert_eq!(corners_of(&pts), vec![g(0, 0), g(2, 0), g(2, 2)]);
455 }
456
457 #[test]
458 fn corners_of_drops_adjacent_duplicates() {
459 let pts = [g(0, 0), g(0, 0), g(3, 0), g(3, 0)];
460 assert_eq!(corners_of(&pts), vec![g(0, 0), g(3, 0)]);
461 }
462
463 #[test]
464 fn corners_of_is_idempotent() {
465 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 3), g(5, 3), g(5, 3)];
466 let once = corners_of(&pts);
467 assert_eq!(corners_of(&once), once);
468 }
469
470 #[test]
471 fn promote_makes_every_bend_a_waypoint() {
472 use crate::edit::create::PathOrdinal;
473 use crate::widget::segmentkind::SegmentKind;
474 use crate::widget::waypoint_router::TaggedPoint;
475 use blockworx_router::point::Point;
476
477 let tp = |x: i32, y: i32| TaggedPoint {
478 segment: SegmentKind::StartToEnd,
479 pos: Point::from(GridPoint { x, y }),
480 };
481 let path = [tp(0, 0), tp(3, 0), tp(3, 3), tp(6, 3)];
483 let stored = loaded_route(&[]);
484 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
485 assert!(stored.is_empty());
486 assert_ne!(
487 PathOrdinal::new(0),
488 PathOrdinal::new(1),
489 "ordinals name distinct corners"
490 );
491
492 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
493
494 assert_eq!(corner_positions(&promoted), vec![g(3, 0), g(3, 3)]);
495 assert!(promoted.iter().all(|wp| !wp.locked));
496 }
497
498 #[test]
499 fn straight_legs_materialize_without_a_router() {
500 let stored = loaded_route(&[(3, 0)]);
501 let mut geometry = RouteGeometry::default();
502 let done = materialize_route(
504 &stored,
505 &mut geometry,
506 Endpoints {
507 start: g(0, 0),
508 end: g(3, 4),
509 },
510 &Obstacles::new(vec![]),
511 None,
512 );
513 assert!(done.is_some(), "a straight route needs no router");
514 let edges: Vec<(GridPoint, GridPoint)> = geometry
515 .iter_edges()
516 .map(|(_, e)| (e.start, e.end))
517 .collect();
518 assert_eq!(edges, vec![(g(0, 0), g(3, 0)), (g(3, 0), g(3, 4))]);
519 }
520
521 #[test]
522 fn a_blocked_leg_defers_when_no_router() {
523 let stored = loaded_route(&[]);
524 let mut geometry = RouteGeometry::default();
525 let obstacles = Obstacles::new(vec![ObstacleRect {
527 top_left: g(4, -2).into(),
528 bottom_right: g(6, 2).into(),
529 }]);
530 let done = materialize_route(
531 &stored,
532 &mut geometry,
533 Endpoints {
534 start: g(0, 0),
535 end: g(10, 0),
536 },
537 &obstacles,
538 None,
539 );
540 assert!(
541 done.is_none(),
542 "a blocked straight leg must defer to the router"
543 );
544 }
545
546 #[test]
547 fn direct_relay_draws_straight_legs_and_leaves_its_corners() {
548 let corners = [g(0, 5)];
549 let mut geometry = RouteGeometry::default();
550 materialize_corners_direct(
551 &corners,
552 &mut geometry,
553 Endpoints {
554 start: g(0, 0),
555 end: g(5, 5),
556 },
557 );
558 let edges: Vec<(GridPoint, GridPoint)> = geometry
559 .iter_edges()
560 .map(|(_, e)| (e.start, e.end))
561 .collect();
562 assert_eq!(edges, vec![(g(0, 0), g(0, 5)), (g(0, 5), g(5, 5))]);
563 assert_eq!(corners, [g(0, 5)]);
566 }
567
568 #[test]
569 fn direct_relay_l_bends_a_diagonal_leg_without_a_router() {
570 let mut geometry = RouteGeometry::default();
571 materialize_corners_direct(
572 &[g(3, 4)],
573 &mut geometry,
574 Endpoints {
575 start: g(0, 0),
576 end: g(3, 8),
577 },
578 );
579 assert!(
580 geometry
581 .iter_edges()
582 .all(|(_, e)| e.start.x == e.end.x || e.start.y == e.end.y),
583 "every relayed edge is axis-aligned — an L, never a diagonal"
584 );
585 assert!(
586 geometry.iter_edges().count() >= 2,
587 "the diagonal leg produced a bend"
588 );
589 }
590
591 #[test]
592 fn a_non_colinear_leg_defers_when_no_router() {
593 let stored = loaded_route(&[(3, 4)]);
595 let mut geometry = RouteGeometry::default();
596 let done = materialize_route(
597 &stored,
598 &mut geometry,
599 Endpoints {
600 start: g(0, 0),
601 end: g(3, 8),
602 },
603 &Obstacles::new(vec![]),
604 None,
605 );
606 assert!(
607 done.is_none(),
608 "a non-colinear leg must defer to the router"
609 );
610 }
611
612 fn pruned(
616 waypoints: &[(i32, i32)],
617 start: GridPoint,
618 end: GridPoint,
619 ) -> (Vec<GridPoint>, bool) {
620 let stored: Vec<Waypoint> = waypoints.iter().map(|&(x, y)| wp(x, y)).collect();
621 let doomed = backtracking_ordinals(&stored, start, end);
622 (
623 corner_positions(&pruned_waypoints(&stored, &doomed)),
624 !doomed.is_empty(),
625 )
626 }
627
628 #[test]
629 fn removes_a_waypoint_the_wire_doubles_back_on() {
630 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(5, 0));
633 assert!(removed);
634 assert!(kept.is_empty());
635 }
636
637 #[test]
638 fn keeps_a_real_corner_and_a_straight_through_pin() {
639 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(10, 5));
641 assert!(!removed);
642 assert_eq!(kept, vec![g(10, 0)]);
643 let (kept, removed) = pruned(&[(5, 0)], g(0, 0), g(10, 0));
646 assert!(!removed);
647 assert_eq!(kept.len(), 1);
648 }
649
650 #[test]
651 fn cascades_after_a_removal_exposes_a_new_reversal() {
652 let (kept, removed) = pruned(&[(10, 0), (3, 0)], g(0, 0), g(5, 0));
655 assert!(removed);
656 assert_eq!(kept, vec![g(3, 0)]);
657 }
658}