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 {
85 self.rects.iter().any(|r| r.hugs_wire(a, b, ROUTE_GUTTER))
86 }
87
88 pub fn accessible(&self, p: GridPoint) -> bool {
90 !self
91 .by_row
92 .get(&p.y)
93 .into_iter()
94 .flatten()
95 .any(|&i| self.rects[i].contains(p.into()))
96 }
97}
98
99fn axis_aligned(a: GridPoint, b: GridPoint) -> bool {
100 a.x == b.x || a.y == b.y
101}
102
103#[derive(Clone, Copy)]
106pub struct Endpoints {
107 pub start: GridPoint,
108 pub end: GridPoint,
109}
110
111pub fn materialize_route(
129 stored: &[Waypoint],
130 geometry: &mut RouteGeometry,
131 ends: Endpoints,
132 obstacles: &Obstacles,
133 mut router: Option<&mut ClosedRouter>,
134) -> Option<Vec<Waypoint>> {
135 let mut corners: Vec<GridPoint> = Vec::with_capacity(stored.len() + 2);
138 corners.push(ends.start);
139 for pos in corner_positions(stored) {
140 if router.is_some() && !obstacles.accessible(pos) {
141 continue;
142 }
143 corners.push(pos);
144 }
145 corners.push(ends.end);
146
147 let mut polyline: Vec<GridPoint> = vec![corners[0]];
150 let mut incoming: Option<Direction> = None;
151 for pair in corners.windows(2) {
152 let (a, b) = (pair[0], pair[1]);
153 if a == b {
154 continue;
155 }
156 if axis_aligned(a, b) && !obstacles.blocked(a, b) && !obstacles.hugs(a, b) {
157 polyline.push(b);
158 incoming = direction_between(a.into(), b.into());
159 continue;
160 }
161 let r = router.as_mut()?;
162 let (subpath, outgoing) = r.route_leg(a.into(), b.into(), incoming);
163 if subpath.len() >= 2 {
164 polyline.extend(subpath[1..].iter().map(|&p| GridPoint::from(p)));
165 } else {
166 polyline.push(b);
167 }
168 incoming = outgoing;
169 }
170
171 let locked = locked_corners(stored);
172 let path = corners_of(&polyline);
173 let promoted = write_geometry(geometry, ends, &path, &locked);
174
175 if let Some(r) = router.as_mut() {
176 add_route_cost(r, geometry.iter_edges().map(|(_, e)| e), WIRE_COST);
177 }
178 Some(promoted)
179}
180
181pub fn materialize_corners_direct(
198 waypoints: &[GridPoint],
199 geometry: &mut RouteGeometry,
200 ends: Endpoints,
201) {
202 let mut corners: Vec<GridPoint> = Vec::with_capacity(waypoints.len() + 2);
203 corners.push(ends.start);
204 corners.extend_from_slice(waypoints);
205 corners.push(ends.end);
206
207 let last_leg = corners.len().saturating_sub(2);
208 let has_waypoints = corners.len() > 2;
209 let mut polyline: Vec<GridPoint> = vec![corners[0]];
210 let mut incoming: Option<Direction> = None;
211 for (i, pair) in corners.windows(2).enumerate() {
212 let (a, b) = (pair[0], pair[1]);
213 if a == b {
214 continue;
215 }
216 if axis_aligned(a, b) {
217 polyline.push(b);
218 } else {
219 let horizontal_first = if has_waypoints && i == 0 {
220 true } else if has_waypoints && i == last_leg {
222 false } else {
224 !matches!(incoming, Some(Direction::North | Direction::South))
225 };
226 let bend = if horizontal_first {
227 GridPoint { x: b.x, y: a.y }
228 } else {
229 GridPoint { x: a.x, y: b.y }
230 };
231 polyline.push(bend);
232 polyline.push(b);
233 }
234 incoming = direction_between(polyline[polyline.len() - 2].into(), b.into());
235 }
236
237 let path = corners_of(&polyline);
238 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
239 for w in path.windows(2) {
240 edges.insert_value(RouteEdge {
241 start: w[0],
242 end: w[1],
243 });
244 }
245 geometry.edges = edges;
246 geometry.start_pos = ends.start;
247 geometry.end_pos = ends.end;
248}
249
250fn write_geometry(
256 geometry: &mut RouteGeometry,
257 ends: Endpoints,
258 path: &[GridPoint],
259 locked: &HashSet<GridPoint>,
260) -> Vec<Waypoint> {
261 geometry.edges = edges_of(path);
262 geometry.start_pos = ends.start;
263 geometry.end_pos = ends.end;
264 interior_waypoints(path, locked)
265}
266
267fn edges_of(path: &[GridPoint]) -> IdMap<EdgeId, RouteEdge> {
269 let mut edges: IdMap<EdgeId, RouteEdge> = IdMap::default();
270 for w in path.windows(2) {
271 edges.insert_value(RouteEdge {
272 start: w[0],
273 end: w[1],
274 });
275 }
276 edges
277}
278
279fn interior_waypoints(path: &[GridPoint], locked: &HashSet<GridPoint>) -> Vec<Waypoint> {
282 if path.len() <= 2 {
283 return Vec::new();
284 }
285 path[1..path.len() - 1]
286 .iter()
287 .map(|&pos| Waypoint {
288 pos,
289 locked: locked.contains(&pos),
290 })
291 .collect()
292}
293
294fn locked_corners(waypoints: &[Waypoint]) -> HashSet<GridPoint> {
296 waypoints
297 .iter()
298 .filter(|wp| wp.locked)
299 .map(|wp| wp.pos)
300 .collect()
301}
302
303pub fn corners_of(points: &[GridPoint]) -> Vec<GridPoint> {
308 let mut out: Vec<GridPoint> = Vec::with_capacity(points.len());
309 for &p in points {
310 if out.last() == Some(&p) {
311 continue;
312 }
313 if let [.., a, b] = out[..]
314 && collinear(a, b, p)
315 {
316 out.pop();
317 }
318 out.push(p);
319 }
320 out
321}
322
323pub fn promote_corners_to_waypoints(
331 stored: &[Waypoint],
332 geometry: &mut RouteGeometry,
333) -> Vec<Waypoint> {
334 let corners = geometry_corners(geometry);
335 let locked = locked_corners(stored);
336
337 geometry.edges = edges_of(&corners);
346 interior_waypoints(&corners, &locked)
347}
348
349pub fn geometry_corners(geometry: &RouteGeometry) -> Vec<GridPoint> {
351 let mut polyline = vec![geometry.start_pos];
352 polyline.extend(geometry.iter_edges().map(|(_, e)| e.end));
353 polyline.push(geometry.end_pos);
354 corners_of(&polyline)
355}
356
357fn collinear(a: GridPoint, b: GridPoint, c: GridPoint) -> bool {
361 (b.x - a.x) as i64 * (c.y - a.y) as i64 == (b.y - a.y) as i64 * (c.x - a.x) as i64
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 use crate::edit::geometry::{backtracking_ordinals, pruned_waypoints};
369
370 fn g(x: i32, y: i32) -> GridPoint {
371 GridPoint { x, y }
372 }
373
374 fn wp(x: i32, y: i32) -> Waypoint {
375 Waypoint {
376 pos: GridPoint { x, y },
377 locked: false,
378 }
379 }
380
381 fn loaded_route(waypoints: &[(i32, i32)]) -> Vec<Waypoint> {
384 waypoints.iter().map(|&(x, y)| wp(x, y)).collect()
385 }
386
387 fn edges_double_back(geometry: &RouteGeometry) -> bool {
388 let edges: Vec<_> = geometry
389 .iter_edges()
390 .map(|(_, e)| (e.start, e.end))
391 .collect();
392 edges.windows(2).any(|w| {
393 let (ax, ay) = (w[0].1.x - w[0].0.x, w[0].1.y - w[0].0.y);
394 let (bx, by) = (w[1].1.x - w[1].0.x, w[1].1.y - w[1].0.y);
395 (ay == 0 && by == 0 && (ax > 0) != (bx > 0))
396 || (ax == 0 && bx == 0 && (ay > 0) != (by > 0))
397 })
398 }
399
400 #[test]
401 fn promote_erases_a_backtracking_spike_from_the_edges() {
402 use crate::edit::create::PathOrdinal;
403 use crate::widget::segmentkind::SegmentKind;
404 use crate::widget::waypoint_router::TaggedPoint;
405 use blockworx_router::point::Point;
406
407 let tp = |segment, x, y| TaggedPoint {
408 segment,
409 pos: Point::from(g(x, y)),
410 };
411 let w = PathOrdinal::new(0);
415 let path = [
416 tp(SegmentKind::StartToWaypoint(w), 0, 0),
417 tp(SegmentKind::StartToWaypoint(w), 10, 0),
418 tp(SegmentKind::WaypointToEnd(w), 10, 0),
419 tp(SegmentKind::WaypointToEnd(w), 5, 0),
420 ];
421 let stored = loaded_route(&[(10, 0)]);
422 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
423 assert!(
424 edges_double_back(&geometry),
425 "setup: the solve kept the reversal spike in the edges"
426 );
427
428 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
429 assert!(
430 !edges_double_back(&geometry),
431 "promote collapsed the spike in the edges"
432 );
433 assert!(promoted.is_empty(), "and dropped the overshoot waypoint");
434 }
435
436 #[test]
437 fn corners_of_drops_collinear_interior_points() {
438 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 1), g(2, 2)];
440 assert_eq!(corners_of(&pts), vec![g(0, 0), g(2, 0), g(2, 2)]);
441 }
442
443 #[test]
444 fn corners_of_drops_adjacent_duplicates() {
445 let pts = [g(0, 0), g(0, 0), g(3, 0), g(3, 0)];
446 assert_eq!(corners_of(&pts), vec![g(0, 0), g(3, 0)]);
447 }
448
449 #[test]
450 fn corners_of_is_idempotent() {
451 let pts = [g(0, 0), g(1, 0), g(2, 0), g(2, 3), g(5, 3), g(5, 3)];
452 let once = corners_of(&pts);
453 assert_eq!(corners_of(&once), once);
454 }
455
456 #[test]
457 fn promote_makes_every_bend_a_waypoint() {
458 use crate::edit::create::PathOrdinal;
459 use crate::widget::segmentkind::SegmentKind;
460 use crate::widget::waypoint_router::TaggedPoint;
461 use blockworx_router::point::Point;
462
463 let tp = |x: i32, y: i32| TaggedPoint {
464 segment: SegmentKind::StartToEnd,
465 pos: Point::from(GridPoint { x, y }),
466 };
467 let path = [tp(0, 0), tp(3, 0), tp(3, 3), tp(6, 3)];
469 let stored = loaded_route(&[]);
470 let mut geometry = crate::widget::auto_route::geometry_from_points(&path);
471 assert!(stored.is_empty());
472 assert_ne!(
473 PathOrdinal::new(0),
474 PathOrdinal::new(1),
475 "ordinals name distinct corners"
476 );
477
478 let promoted = promote_corners_to_waypoints(&stored, &mut geometry);
479
480 assert_eq!(corner_positions(&promoted), vec![g(3, 0), g(3, 3)]);
481 assert!(promoted.iter().all(|wp| !wp.locked));
482 }
483
484 #[test]
485 fn straight_legs_materialize_without_a_router() {
486 let stored = loaded_route(&[(3, 0)]);
487 let mut geometry = RouteGeometry::default();
488 let done = materialize_route(
490 &stored,
491 &mut geometry,
492 Endpoints {
493 start: g(0, 0),
494 end: g(3, 4),
495 },
496 &Obstacles::new(vec![]),
497 None,
498 );
499 assert!(done.is_some(), "a straight route needs no router");
500 let edges: Vec<(GridPoint, GridPoint)> = geometry
501 .iter_edges()
502 .map(|(_, e)| (e.start, e.end))
503 .collect();
504 assert_eq!(edges, vec![(g(0, 0), g(3, 0)), (g(3, 0), g(3, 4))]);
505 }
506
507 #[test]
508 fn a_blocked_leg_defers_when_no_router() {
509 let stored = loaded_route(&[]);
510 let mut geometry = RouteGeometry::default();
511 let obstacles = Obstacles::new(vec![ObstacleRect {
513 top_left: g(4, -2).into(),
514 bottom_right: g(6, 2).into(),
515 }]);
516 let done = materialize_route(
517 &stored,
518 &mut geometry,
519 Endpoints {
520 start: g(0, 0),
521 end: g(10, 0),
522 },
523 &obstacles,
524 None,
525 );
526 assert!(
527 done.is_none(),
528 "a blocked straight leg must defer to the router"
529 );
530 }
531
532 #[test]
533 fn direct_relay_draws_straight_legs_and_leaves_its_corners() {
534 let corners = [g(0, 5)];
535 let mut geometry = RouteGeometry::default();
536 materialize_corners_direct(
537 &corners,
538 &mut geometry,
539 Endpoints {
540 start: g(0, 0),
541 end: g(5, 5),
542 },
543 );
544 let edges: Vec<(GridPoint, GridPoint)> = geometry
545 .iter_edges()
546 .map(|(_, e)| (e.start, e.end))
547 .collect();
548 assert_eq!(edges, vec![(g(0, 0), g(0, 5)), (g(0, 5), g(5, 5))]);
549 assert_eq!(corners, [g(0, 5)]);
552 }
553
554 #[test]
555 fn direct_relay_l_bends_a_diagonal_leg_without_a_router() {
556 let mut geometry = RouteGeometry::default();
557 materialize_corners_direct(
558 &[g(3, 4)],
559 &mut geometry,
560 Endpoints {
561 start: g(0, 0),
562 end: g(3, 8),
563 },
564 );
565 assert!(
566 geometry
567 .iter_edges()
568 .all(|(_, e)| e.start.x == e.end.x || e.start.y == e.end.y),
569 "every relayed edge is axis-aligned — an L, never a diagonal"
570 );
571 assert!(
572 geometry.iter_edges().count() >= 2,
573 "the diagonal leg produced a bend"
574 );
575 }
576
577 #[test]
578 fn a_non_colinear_leg_defers_when_no_router() {
579 let stored = loaded_route(&[(3, 4)]);
581 let mut geometry = RouteGeometry::default();
582 let done = materialize_route(
583 &stored,
584 &mut geometry,
585 Endpoints {
586 start: g(0, 0),
587 end: g(3, 8),
588 },
589 &Obstacles::new(vec![]),
590 None,
591 );
592 assert!(
593 done.is_none(),
594 "a non-colinear leg must defer to the router"
595 );
596 }
597
598 fn pruned(
602 waypoints: &[(i32, i32)],
603 start: GridPoint,
604 end: GridPoint,
605 ) -> (Vec<GridPoint>, bool) {
606 let stored: Vec<Waypoint> = waypoints.iter().map(|&(x, y)| wp(x, y)).collect();
607 let doomed = backtracking_ordinals(&stored, start, end);
608 (
609 corner_positions(&pruned_waypoints(&stored, &doomed)),
610 !doomed.is_empty(),
611 )
612 }
613
614 #[test]
615 fn removes_a_waypoint_the_wire_doubles_back_on() {
616 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(5, 0));
619 assert!(removed);
620 assert!(kept.is_empty());
621 }
622
623 #[test]
624 fn keeps_a_real_corner_and_a_straight_through_pin() {
625 let (kept, removed) = pruned(&[(10, 0)], g(0, 0), g(10, 5));
627 assert!(!removed);
628 assert_eq!(kept, vec![g(10, 0)]);
629 let (kept, removed) = pruned(&[(5, 0)], g(0, 0), g(10, 0));
632 assert!(!removed);
633 assert_eq!(kept.len(), 1);
634 }
635
636 #[test]
637 fn cascades_after_a_removal_exposes_a_new_reversal() {
638 let (kept, removed) = pruned(&[(10, 0), (3, 0)], g(0, 0), g(5, 0));
641 assert!(removed);
642 assert_eq!(kept, vec![g(3, 0)]);
643 }
644}