LCOV - code coverage report
Current view: top level - dashql-dataflow/src/ortho_router - grid.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 98.4 % 192 189
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! The routing grid and the search over it.
       2              : //!
       3              : //! The grid is the Hanan grid of the obstacles grown by the margin: one
       4              : //! vertical line through each grown obstacle's left and right edges and
       5              : //! through each port's stub end, one horizontal line through each top
       6              : //! and bottom edge and each stub end. Its vertices are where the lines
       7              : //! cross, its edges join neighbouring vertices along a line. No obstacle
       8              : //! edge lies strictly between two neighbouring lines, so a grid edge
       9              : //! either crosses an obstacle's interior over its whole length or not at
      10              : //! all, and an edge along an obstacle's boundary is free: routes run
      11              : //! along the margin lines, at the margin's distance from the nodes.
      12              : //!
      13              : //! A route is a shortest path in that grid where each turn costs
      14              : //! [`Options::bend_penalty`](super::Options::bend_penalty) on top of the
      15              : //! length: A* over (vertex, direction) states, with the Manhattan
      16              : //! distance to the goal as its heuristic (a lower bound, since a bend
      17              : //! never costs less than nothing).
      18              : 
      19              : use std::cmp::Ordering;
      20              : use std::collections::BinaryHeap;
      21              : 
      22              : use super::geom::{EPS, Point, Rect, Side};
      23              : 
      24              : /// Sorted distinct coordinates, those within [`EPS`] of the previous
      25              : /// one dropped.
      26           37 : fn lines(mut v: Vec<f32>) -> Vec<f32> {
      27         1071 :     v.retain(|x| x.is_finite());
      28           37 :     v.sort_by(f32::total_cmp);
      29           37 :     let mut out: Vec<f32> = Vec::with_capacity(v.len());
      30         1070 :     for x in v {
      31         1070 :         if out.last().is_none_or(|l| x - l > EPS) {
      32          253 :             out.push(x);
      33          817 :         }
      34              :     }
      35           37 :     out
      36           37 : }
      37              : 
      38              : /// The range of lines in `[lo, hi]` (within [`EPS`]).
      39          312 : fn closed(v: &[f32], lo: f32, hi: f32) -> (usize, usize) {
      40              :     (
      41         1583 :         v.partition_point(|&x| x < lo - EPS),
      42         1583 :         v.partition_point(|&x| x <= hi + EPS),
      43              :     )
      44          312 : }
      45              : 
      46              : /// The range of lines strictly inside `(lo, hi)` (within [`EPS`]).
      47          312 : fn open(v: &[f32], lo: f32, hi: f32) -> (usize, usize) {
      48              :     (
      49         1583 :         v.partition_point(|&x| x <= lo + EPS),
      50         1583 :         v.partition_point(|&x| x < hi - EPS),
      51              :     )
      52          312 : }
      53              : 
      54              : /// The index of the line at `x`, if there is one.
      55          442 : fn line_of(v: &[f32], x: f32) -> Option<usize> {
      56         2316 :     let i = v.partition_point(|&a| a < x - EPS);
      57          442 :     (i < v.len() && (v[i] - x).abs() <= EPS).then_some(i)
      58          442 : }
      59              : 
      60              : /// The routing grid.
      61              : #[derive(Clone, Debug, Default)]
      62              : pub(crate) struct Grid {
      63              :     xs: Vec<f32>,
      64              :     ys: Vec<f32>,
      65              :     /// Vertices strictly inside an obstacle, by `j * nx + i`.
      66              :     blocked: Vec<bool>,
      67              :     /// Edges from `(i, j)` to `(i + 1, j)` through an obstacle.
      68              :     h_blocked: Vec<bool>,
      69              :     /// Edges from `(i, j)` to `(i, j + 1)` through an obstacle.
      70              :     v_blocked: Vec<bool>,
      71              : }
      72              : 
      73              : impl Grid {
      74              :     /// The grid of grown obstacles, with lines through `points` too.
      75           18 :     pub(crate) fn build(obstacles: &[Rect], points: &[Point]) -> Grid {
      76           18 :         let xs = lines(
      77           18 :             obstacles
      78           18 :                 .iter()
      79          156 :                 .flat_map(|o| [o.min.x, o.max.x])
      80           18 :                 .chain(points.iter().map(|p| p.x))
      81           18 :                 .collect(),
      82              :         );
      83           18 :         let ys = lines(
      84           18 :             obstacles
      85           18 :                 .iter()
      86          156 :                 .flat_map(|o| [o.min.y, o.max.y])
      87           18 :                 .chain(points.iter().map(|p| p.y))
      88           18 :                 .collect(),
      89              :         );
      90           18 :         let nx = xs.len();
      91           18 :         let n = nx * ys.len();
      92           18 :         let mut grid = Grid {
      93           18 :             blocked: vec![false; n],
      94           18 :             h_blocked: vec![false; n],
      95           18 :             v_blocked: vec![false; n],
      96           18 :             xs,
      97           18 :             ys,
      98           18 :         };
      99          156 :         for o in obstacles {
     100          156 :             let (cxa, cxb) = closed(&grid.xs, o.min.x, o.max.x);
     101          156 :             let (cya, cyb) = closed(&grid.ys, o.min.y, o.max.y);
     102          156 :             let (oxa, oxb) = open(&grid.xs, o.min.x, o.max.x);
     103          156 :             let (oya, oyb) = open(&grid.ys, o.min.y, o.max.y);
     104          187 :             for j in oya..oyb {
     105          216 :                 for i in cxa..cxb.saturating_sub(1) {
     106          216 :                     grid.h_blocked[j * nx + i] = true;
     107          216 :                 }
     108          187 :                 for i in oxa..oxb {
     109           29 :                     grid.blocked[j * nx + i] = true;
     110           29 :                 }
     111              :             }
     112          156 :             for i in oxa..oxb {
     113           44 :                 for j in cya..cyb.saturating_sub(1) {
     114           44 :                     grid.v_blocked[j * nx + i] = true;
     115           44 :                 }
     116              :             }
     117              :         }
     118           18 :         grid
     119           18 :     }
     120              : 
     121        26552 :     fn nx(&self) -> usize {
     122        26552 :         self.xs.len()
     123        26552 :     }
     124              : 
     125              :     /// The vertex at `p`, when `p` is on two lines and outside every
     126              :     /// obstacle.
     127          221 :     pub(crate) fn vertex(&self, p: Point) -> Option<usize> {
     128          221 :         let i = line_of(&self.xs, p.x)?;
     129          221 :         let j = line_of(&self.ys, p.y)?;
     130          221 :         let v = j * self.nx() + i;
     131          221 :         (!self.blocked[v]).then_some(v)
     132          221 :     }
     133              : 
     134              :     /// Where a vertex is.
     135        11227 :     pub(crate) fn point(&self, v: usize) -> Point {
     136        11227 :         Point::new(self.xs[v % self.nx()], self.ys[v / self.nx()])
     137        11227 :     }
     138              : 
     139              :     /// The vertex next to `v` going `d`, when the way is free.
     140         3877 :     fn step(&self, v: usize, d: Side) -> Option<usize> {
     141         3877 :         let nx = self.nx();
     142         3877 :         let (i, j) = (v % nx, v / nx);
     143         3432 :         let next = match d {
     144         1081 :             Side::Left if i > 0 && !self.h_blocked[v - 1] => v - 1,
     145         1278 :             Side::Right if i + 1 < nx && !self.h_blocked[v] => v + 1,
     146          760 :             Side::Top if j > 0 && !self.v_blocked[v - nx] => v - nx,
     147          758 :             Side::Bottom if j + 1 < self.ys.len() && !self.v_blocked[v] => v + nx,
     148          445 :             _ => return None,
     149              :         };
     150         3432 :         (!self.blocked[next]).then_some(next)
     151         3877 :     }
     152              : }
     153              : 
     154              : /// An open state, the cheapest estimate first.
     155              : #[derive(Clone, Copy, Debug, PartialEq)]
     156              : struct Open {
     157              :     f: f32,
     158              :     state: u32,
     159              : }
     160              : 
     161              : impl Eq for Open {}
     162              : 
     163              : impl Ord for Open {
     164        12224 :     fn cmp(&self, other: &Self) -> Ordering {
     165              :         // A max-heap: reversed, and ties broken by the state so the
     166              :         // order (and the route) only depends on the input.
     167        12224 :         other
     168        12224 :             .f
     169        12224 :             .total_cmp(&self.f)
     170        12224 :             .then_with(|| other.state.cmp(&self.state))
     171        12224 :     }
     172              : }
     173              : 
     174              : impl PartialOrd for Open {
     175        12224 :     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
     176        12224 :         Some(self.cmp(other))
     177        12224 :     }
     178              : }
     179              : 
     180              : /// The search's buffers, kept between searches.
     181              : #[derive(Clone, Debug, Default)]
     182              : pub(crate) struct Search {
     183              :     g: Vec<f32>,
     184              :     parent: Vec<u32>,
     185              :     done: Vec<bool>,
     186              :     heap: BinaryHeap<Open>,
     187              : }
     188              : 
     189              : const NONE: u32 = u32::MAX;
     190              : 
     191              : impl Search {
     192              :     /// The cheapest path from `start`, leaving in direction `first`, to
     193              :     /// `goal`, arriving in direction `last`: its vertices, from `start`
     194              :     /// to `goal`. `None` when `goal` cannot be reached.
     195          108 :     pub(crate) fn find(
     196          108 :         &mut self,
     197          108 :         grid: &Grid,
     198          108 :         start: usize,
     199          108 :         first: Side,
     200          108 :         goal: usize,
     201          108 :         last: Side,
     202          108 :         bend: f32,
     203          108 :     ) -> Option<Vec<usize>> {
     204          108 :         let states = grid.blocked.len() * 4;
     205          108 :         if states > NONE as usize {
     206            0 :             return None;
     207          108 :         }
     208          108 :         self.g.clear();
     209          108 :         self.g.resize(states, f32::INFINITY);
     210          108 :         self.parent.clear();
     211          108 :         self.parent.resize(states, NONE);
     212          108 :         self.done.clear();
     213          108 :         self.done.resize(states, false);
     214          108 :         self.heap.clear();
     215              : 
     216          108 :         let target = grid.point(goal);
     217         3501 :         let h = |v: usize| {
     218         3501 :             let p = grid.point(v);
     219         3501 :             (p.x - target.x).abs() + (p.y - target.y).abs()
     220         3501 :         };
     221          108 :         let s0 = start * 4 + first.index();
     222          108 :         self.g[s0] = 0.0;
     223          108 :         self.heap.push(Open {
     224          108 :             f: h(start),
     225          108 :             state: s0 as u32,
     226          108 :         });
     227          108 :         let mut best = (f32::INFINITY, NONE);
     228         1511 :         while let Some(Open { f, state }) = self.heap.pop() {
     229         1511 :             if f >= best.0 {
     230          108 :                 break;
     231         1403 :             }
     232         1403 :             let s = state as usize;
     233         1403 :             if self.done[s] {
     234            0 :                 continue;
     235         1403 :             }
     236         1403 :             self.done[s] = true;
     237         1403 :             let (v, d) = (s / 4, Side::ALL[s % 4]);
     238         1403 :             let g = self.g[s];
     239         1403 :             if v == goal {
     240          112 :                 let total = g + if d == last { 0.0 } else { bend };
     241          112 :                 if total < best.0 {
     242          108 :                     best = (total, state);
     243          108 :                 }
     244          112 :                 continue;
     245         1291 :             }
     246         5164 :             for nd in Side::ALL {
     247         5164 :                 if nd == d.opposite() {
     248         1291 :                     continue;
     249         3873 :                 }
     250         3873 :                 let Some(n) = grid.step(v, nd) else { continue };
     251         3430 :                 let ns = n * 4 + nd.index();
     252         3430 :                 if self.done[ns] {
     253           13 :                     continue;
     254         3417 :                 }
     255         3417 :                 let (a, b) = (grid.point(v), grid.point(n));
     256         3417 :                 let len = (a.x - b.x).abs() + (a.y - b.y).abs();
     257         3417 :                 let cost = g + len + if nd == d { 0.0 } else { bend };
     258         3417 :                 if cost < self.g[ns] {
     259         3393 :                     self.g[ns] = cost;
     260         3393 :                     self.parent[ns] = state;
     261         3393 :                     self.heap.push(Open {
     262         3393 :                         f: cost + h(n),
     263         3393 :                         state: ns as u32,
     264         3393 :                     });
     265         3393 :                 }
     266              :             }
     267              :         }
     268          108 :         if best.1 == NONE {
     269            0 :             return None;
     270          108 :         }
     271          108 :         let mut path = Vec::new();
     272          108 :         let mut s = best.1;
     273          892 :         while s != NONE {
     274          784 :             path.push(s as usize / 4);
     275          784 :             s = self.parent[s as usize];
     276          784 :         }
     277          108 :         path.reverse();
     278          108 :         Some(path)
     279          108 :     }
     280              : }
     281              : 
     282              : #[cfg(test)]
     283              : mod tests {
     284              :     use super::*;
     285              : 
     286              :     #[test]
     287            1 :     fn close_coordinates_make_one_line() {
     288            1 :         assert_eq!(
     289            1 :             lines(vec![2.0, 1.0, 1.0005, f32::NAN, 3.0]),
     290            1 :             vec![1.0, 2.0, 3.0]
     291              :         );
     292            1 :     }
     293              : 
     294              :     #[test]
     295            1 :     fn an_obstacle_blocks_its_inside_and_frees_its_edges() {
     296            1 :         let o = Rect::from_corners([0.0, 0.0], [10.0, 10.0]);
     297            1 :         let g = Grid::build(&[o], &[Point::new(5.0, 5.0), Point::new(20.0, 20.0)]);
     298              :         // The centre is inside.
     299            1 :         assert!(g.vertex(Point::new(5.0, 5.0)).is_none());
     300              :         // A corner is on the edge: free, and so is the way along it.
     301            1 :         let corner = g.vertex(Point::new(0.0, 0.0)).unwrap();
     302            1 :         assert!(g.step(corner, Side::Right).is_some());
     303            1 :         assert!(g.step(corner, Side::Bottom).is_some());
     304              :         // Going in from the middle of an edge is blocked.
     305            1 :         let mid = g.vertex(Point::new(5.0, 0.0)).unwrap();
     306            1 :         assert!(g.step(mid, Side::Bottom).is_none());
     307            1 :         assert!(g.step(mid, Side::Top).is_none(), "no line above");
     308            1 :     }
     309              : }
        

Generated by: LCOV version 2.0-1