Skip to main content

blockworx/router/
mod.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use blockworx_geom::Pos2;
4use pathfinding::directed::dijkstra::dijkstra;
5use petgraph::{
6    graph::{NodeIndex, UnGraph},
7    visit::EdgeRef,
8};
9
10use crate::router::{
11    block::{Block, ROUTE_GUTTER},
12    channel::{Channel, ChannelOrientation, h_channel, v_channel},
13    coord::{CoordX, CoordY, INFINITY_X, INFINITY_Y, NEG_INFINITY_X, NEG_INFINITY_Y},
14    cost::{COST_ZERO, Cost},
15    event::{Event, EventSense},
16    point::{Point, point},
17    segment::{HSegment, Segment, VSegment, hseg, vseg},
18    turtle::{Mark, Turtle},
19};
20pub mod block;
21pub mod channel;
22pub mod coord;
23pub mod cost;
24pub mod event;
25pub mod point;
26pub mod segment;
27pub mod turtle;
28
29#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
30pub enum Direction {
31    North,
32    South,
33    East,
34    West,
35}
36
37impl Direction {
38    fn opposite(self) -> Self {
39        match self {
40            Direction::North => Direction::South,
41            Direction::South => Direction::North,
42            Direction::East => Direction::West,
43            Direction::West => Direction::East,
44        }
45    }
46}
47
48/// The cardinal direction of travel from `from` to `to`, or `None` if the points
49/// coincide. Matches the convention used by [`RouterNG::successors`] (greater `y`
50/// is `South`).
51pub fn direction_between(from: Point, to: Point) -> Option<Direction> {
52    if to.x > from.x {
53        Some(Direction::East)
54    } else if to.x < from.x {
55        Some(Direction::West)
56    } else if to.y > from.y {
57        Some(Direction::South)
58    } else if to.y < from.y {
59        Some(Direction::North)
60    } else {
61        None
62    }
63}
64
65const TURN_COST: Cost = Cost::new(25.0);
66const MOVE_COST: Cost = Cost::new(1.0);
67pub const WIRE_COST: Cost = Cost::new(10.0);
68
69fn cross_cost(
70    from: Option<Direction>,
71    to: Direction,
72    cost_to_cross_east_west: Cost,
73    cost_to_cross_north_south: Cost,
74) -> Cost {
75    if let Some(from_dir) = from {
76        match (from_dir, to) {
77            (Direction::North, Direction::South) | (Direction::South, Direction::North) => {
78                cost_to_cross_east_west
79            }
80            (Direction::East, Direction::West) | (Direction::West, Direction::East) => {
81                cost_to_cross_north_south
82            }
83            _ => COST_ZERO,
84        }
85    } else {
86        COST_ZERO
87    }
88}
89
90fn turn_cost(from: Option<Direction>, to: Direction) -> Cost {
91    if let Some(from_dir) = from {
92        if from_dir == to {
93            COST_ZERO
94        } else if to == from_dir.opposite() {
95            TURN_COST * 100.0
96        } else {
97            TURN_COST
98        }
99    } else {
100        COST_ZERO
101    }
102}
103
104#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)]
105struct SearchState {
106    node: NodeIndex,
107    dir: Option<Direction>,
108}
109
110#[derive(Debug, Clone, Default)]
111pub struct RouterNGBuilder {
112    /// The blocking rectangles
113    blocks: Vec<Block>,
114    /// The routing channels
115    channels: Vec<Channel>,
116    /// Points at which [`Self::build_closed`] seeds a full (H+V) channel — route
117    /// endpoints and waypoints, whose channels the old per-route path added at
118    /// route time via `seed_channels`. Ignored by the test-only `build`.
119    seed_points: Vec<Point>,
120}
121
122impl RouterNGBuilder {
123    pub fn add_h_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
124        self.channels.push(h_channel(seed, cost));
125    }
126    /// Register a point (a route endpoint or waypoint) at which `build_closed`
127    /// seeds a full channel, so the closed graph already contains the nodes every
128    /// route needs — no geometry is added once the graph is closed.
129    pub fn add_seed_point(&mut self, p: impl Into<Point>) {
130        self.seed_points.push(p.into());
131    }
132    pub fn add_v_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
133        self.channels.push(v_channel(seed, cost));
134    }
135    fn add_routing_moat(
136        &mut self,
137        top_left: Point,
138        bottom_right: Point,
139        distance: i32,
140        cost: Cost,
141    ) {
142        let min_x = top_left.x.min(bottom_right.x);
143        let max_x = top_left.x.max(bottom_right.x);
144        let min_y = top_left.y.min(bottom_right.y);
145        let max_y = top_left.y.max(bottom_right.y);
146        self.add_v_channel(point(min_x - distance - 2, min_y), cost);
147        self.add_v_channel(point(min_x - distance - 2, max_y), cost);
148        self.add_v_channel(point(max_x + distance + 2, min_y), cost);
149        self.add_v_channel(point(max_x + distance + 2, max_y), cost);
150        self.add_h_channel(point(min_x, min_y - distance - 2), cost);
151        self.add_h_channel(point(max_x, min_y - distance - 2), cost);
152        self.add_h_channel(point(min_x, max_y + distance + 2), cost);
153        self.add_h_channel(point(max_x, max_y + distance + 2), cost);
154    }
155    pub fn add_block(&mut self, top_left: impl Into<Point>, bottom_right: impl Into<Point>) {
156        let top_left: Point = top_left.into();
157        let bottom_right: Point = bottom_right.into();
158        let min_x = top_left.x.min(bottom_right.x);
159        let max_x = top_left.x.max(bottom_right.x);
160        let min_y = top_left.y.min(bottom_right.y);
161        let max_y = top_left.y.max(bottom_right.y);
162        let block = Block {
163            top_left: point(min_x, min_y),
164            bottom_right: point(max_x, max_y),
165        };
166        self.blocks.push(block);
167        // Add the routing channels around the blocked rectangle.
168        for moat_lane in 0..5 {
169            let cost = if moat_lane == 0 {
170                Cost::new(0.2)
171            } else {
172                Cost::new(0.1)
173            };
174            self.add_routing_moat(top_left, bottom_right, moat_lane, cost);
175        }
176    }
177    /// Construct a dirty router with all channels seeded but not yet built into a
178    /// graph. Shared by [`Self::build_closed`] and the test-only `build`.
179    fn seed_channels_into_router(&self) -> RouterNG {
180        let block_index = BlockAxisIndex::build(&self.blocks);
181        let mut router = RouterNG {
182            blocks: self.blocks.clone(),
183            block_index,
184            h_segments: BTreeMap::new(),
185            v_segments: BTreeMap::new(),
186            nodes: BTreeSet::new(),
187            graph: UnGraph::default(),
188            node_to_index: BTreeMap::new(),
189            dirty: true,
190        };
191        for channel in &self.channels {
192            match channel.orientation {
193                ChannelOrientation::Horizontal => {
194                    router.seed_horiz_channel(channel.seed, channel.cost);
195                }
196                ChannelOrientation::Vertical => {
197                    router.seed_vert_channel(channel.seed, channel.cost);
198                }
199            }
200        }
201        router
202    }
203    /// Build the mutable [`RouterNG`] directly. Only the router's own unit tests
204    /// construct one this way now; production code uses [`Self::build_closed`].
205    #[cfg(test)]
206    pub fn build(self) -> RouterNG {
207        let mut router = self.seed_channels_into_router();
208        router.update();
209        router
210    }
211    /// Build a [`ClosedRouter`]: seed all channels AND the endpoint/waypoint
212    /// channels registered via [`Self::add_seed_point`], build the graph once,
213    /// then freeze the geometry. Routing on the result only mutates edge weights.
214    pub fn build_closed(self) -> ClosedRouter {
215        let mut router = self.seed_channels_into_router();
216        for &p in &self.seed_points {
217            router.seed_channels(p, COST_ZERO);
218        }
219        router.update();
220        ClosedRouter { inner: router }
221    }
222}
223
224/// A [`RouterNG`] whose geometry has been built and frozen: it exposes routing
225/// and in-place edge-weight mutation, but no method to add segments/channels or
226/// rebuild the graph. Occupancy of a completed route is applied by adding
227/// [`WIRE_COST`] to the existing graph edges the wire covers, so no rebuild is
228/// needed (see Finding 1 in TUNING.md).
229#[derive(Debug, Clone)]
230pub struct ClosedRouter {
231    inner: RouterNG,
232}
233
234impl ClosedRouter {
235    /// Route one leg from `start` to `end`, threading `incoming` (the direction
236    /// already travelled) so the leg can't double back. Read-only w.r.t.
237    /// geometry — the graph is already closed, so the internal `update()` is a
238    /// no-op. Returns the path points and the outgoing direction.
239    pub fn route_leg(
240        &mut self,
241        start: Point,
242        end: Point,
243        incoming: Option<Direction>,
244    ) -> (Vec<Point>, Option<Direction>) {
245        self.inner.path_find_with_fallback(start, end, incoming)
246    }
247
248    /// Add `cost` to every graph edge along the (axis-aligned) `path`, leg by leg.
249    /// The in-place equivalent of the old `add_subpath_cost`.
250    pub fn bump_leg(&mut self, path: &[Point], cost: Cost) {
251        for w in path.windows(2) {
252            self.add_wire_cost(w[0], w[1], cost);
253        }
254    }
255
256    /// Occupy the axis-aligned wire `a → b` by adding `cost` to each of the
257    /// node-to-node graph edges it covers. Walks adjacency in the travel
258    /// direction (edges are split at every node, so each hop is one graph edge).
259    /// Never changes topology or sets `dirty`, so the graph stays closed and
260    /// `successors` reads the new weights live on the next route.
261    pub fn add_wire_cost(&mut self, a: Point, b: Point, cost: Cost) {
262        let Some(dir) = direction_between(a, b) else {
263            return;
264        };
265        let Some(&target) = self.inner.node_to_index.get(&b) else {
266            return;
267        };
268        let Some(&start) = self.inner.node_to_index.get(&a) else {
269            return;
270        };
271        let mut cur = start;
272        let mut cur_pt = a;
273        // Bounded by the number of nodes; the guard only trips on a malformed
274        // (disconnected) graph, in which case we stop rather than loop forever.
275        for _ in 0..self.inner.node_to_index.len() {
276            if cur == target {
277                break;
278            }
279            let step = self.inner.graph.edges(cur).find_map(|e| {
280                let np = self.inner.point(e.target());
281                (direction_between(cur_pt, np) == Some(dir)).then_some((e.id(), e.target(), np))
282            });
283            let Some((edge_id, next, next_pt)) = step else {
284                break;
285            };
286            if let Some(w) = self.inner.graph.edge_weight_mut(edge_id) {
287                *w += cost;
288            }
289            cur = next;
290            cur_pt = next_pt;
291        }
292    }
293
294    /// Whether the axis-aligned wire `a → b` crosses any blocked rectangle.
295    pub fn is_wire_blocked(&self, a: Point, b: Point) -> bool {
296        self.inner
297            .blocks
298            .iter()
299            .any(|block| block.intersects_edge(a, b))
300    }
301
302    /// Whether the straight wire `a → b` hugs any block (runs within the routing
303    /// gutter alongside an edge). Complements [`Self::is_wire_blocked`]: together
304    /// they decide whether a straight leg may be taken verbatim or must route around.
305    pub fn wire_hugs_block(&self, a: Point, b: Point) -> bool {
306        self.inner
307            .blocks
308            .iter()
309            .any(|blk| blk.hugs_wire(a, b, ROUTE_GUTTER))
310    }
311
312    pub fn is_accessible(&self, test: impl Into<Point>) -> bool {
313        self.inner.is_accessible(test)
314    }
315
316    pub fn debug_marks(&self) -> Vec<Mark> {
317        self.inner.debug_marks()
318    }
319}
320
321/// A per-axis index of the (immutable) block set, so channel seeding iterates only
322/// the blocks that intersect a query coordinate instead of scanning them all. Built
323/// once when the closed geometry is assembled. `by_y[y]` lists the blocks whose
324/// vertical extent (expanded by one, matching [`RouterNG::seed_horiz_channel`])
325/// contains `y`; `by_x[x]` the blocks whose horizontal extent (expanded by one)
326/// contains `x`. Same coordinate-keyed `BTreeMap<Coord, Vec<_>>` shape as the
327/// segment maps.
328#[derive(Debug, Clone, Default)]
329struct BlockAxisIndex {
330    by_y: BTreeMap<CoordY, Vec<usize>>,
331    by_x: BTreeMap<CoordX, Vec<usize>>,
332}
333
334impl BlockAxisIndex {
335    fn build(blocks: &[Block]) -> Self {
336        let mut by_y: BTreeMap<CoordY, Vec<usize>> = BTreeMap::new();
337        let mut by_x: BTreeMap<CoordX, Vec<usize>> = BTreeMap::new();
338        for (i, block) in blocks.iter().enumerate() {
339            let ey = block.expand_y(1);
340            for y in ey.top_left.y.raw()..=ey.bottom_right.y.raw() {
341                by_y.entry(CoordY::from(y)).or_default().push(i);
342            }
343            let ex = block.expand_x(1);
344            for x in ex.top_left.x.raw()..=ex.bottom_right.x.raw() {
345                by_x.entry(CoordX::from(x)).or_default().push(i);
346            }
347        }
348        Self { by_y, by_x }
349    }
350
351    /// The blocks whose one-expanded vertical extent contains `y`.
352    fn spanning_y<'a>(&'a self, y: CoordY, blocks: &'a [Block]) -> impl Iterator<Item = &'a Block> {
353        self.by_y.get(&y).into_iter().flatten().map(|&i| &blocks[i])
354    }
355
356    /// The blocks whose one-expanded horizontal extent contains `x`.
357    fn spanning_x<'a>(&'a self, x: CoordX, blocks: &'a [Block]) -> impl Iterator<Item = &'a Block> {
358        self.by_x.get(&x).into_iter().flatten().map(|&i| &blocks[i])
359    }
360}
361
362#[derive(Debug, Clone)]
363pub struct RouterNG {
364    /// The blocking rectangles - not mutable
365    blocks: Vec<Block>,
366    /// Per-axis index of `blocks` for channel seeding (built from `blocks`).
367    block_index: BlockAxisIndex,
368    /// Horizontal segments, keyed by their vertical coordinate
369    h_segments: BTreeMap<CoordY, Vec<HSegment>>,
370    /// Vertical segments, keyed by their horizontal coordinate
371    v_segments: BTreeMap<CoordX, Vec<VSegment>>,
372    /// Nodes: the intersection points of the segments
373    nodes: BTreeSet<Point>,
374    /// The graph to be used for pathfinding, built from the segments and nodes
375    graph: UnGraph<Point, Cost>,
376    /// A map from node to index in the graph, for quick lookup
377    node_to_index: BTreeMap<Point, petgraph::graph::NodeIndex>,
378    /// Dirty flag that indicates `h_segments` or `v_segments` have been modified and the graph needs to be rebuilt.
379    dirty: bool,
380}
381
382impl RouterNG {
383    pub fn debug_marks(&self) -> Vec<Mark> {
384        assert!(
385            !self.dirty,
386            "Cannot generate debug marks when the graph is dirty"
387        );
388        let mut turtle = Turtle::default();
389        for node in self.graph.node_indices() {
390            let pos = self.point(node);
391            turtle.move_to(pos.into());
392            turtle.circle(blockworx_geom::WorldPx::new(3.0));
393            // To make the edges more visible, we add a gap at the beginning and
394            // end of the edge line, so that it looks like this * ---- * rather than this *------------------*
395            for edge in self.graph.edges(node) {
396                let target = edge.target();
397                let edge_weight = edge.weight();
398                let target_pos = self.point(target);
399
400                // Calculate direction and distance
401                let start_pos: Pos2 = pos.into();
402                let end_pos: Pos2 = target_pos.into();
403                let dx = end_pos.x - start_pos.x;
404                let dy = end_pos.y - start_pos.y;
405                let distance = (dx * dx + dy * dy).sqrt();
406
407                // Skip very short edges
408                if distance < 8.0 {
409                    continue;
410                }
411
412                // Create 4-pixel gap at each end
413                let gap = 4.0;
414                let gap_ratio = gap / distance;
415
416                // Start point with gap from the node
417                let line_start =
418                    Pos2::new(start_pos.x + dx * gap_ratio, start_pos.y + dy * gap_ratio);
419
420                // End point with gap before the target
421                let line_end = Pos2::new(end_pos.x - dx * gap_ratio, end_pos.y - dy * gap_ratio);
422
423                turtle.move_to(line_start);
424                turtle.line_to(line_end);
425                let mid_point = line_start + (line_end - line_start) / 2.0;
426                let weight: f64 = (*edge_weight).into();
427                turtle.label(mid_point, weight as f32);
428            }
429        }
430        turtle.compile()
431    }
432    pub fn is_accessible(&self, test: impl Into<Point>) -> bool {
433        let test: Point = test.into();
434        !self.blocks.iter().any(|block| block.contains(test))
435    }
436    fn seed_horiz_channel(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
437        let center: Point = center.into();
438        let cost: Cost = cost.into();
439        let mut left_endpoint = NEG_INFINITY_X;
440        let mut right_endpoint = INFINITY_X;
441        // Only blocks spanning `center.y` can clip a horizontal channel there; the
442        // index yields exactly those (matching the old `expand_y(1).spans_y` gate).
443        for block in self.block_index.spanning_y(center.y, &self.blocks) {
444            let block = block.expand_x(1).expand_y(1);
445            if block.spans_x(center.x) {
446                // Seed point is inside a block, so no channel can pass through here.
447                return;
448            }
449            if block.is_left_of(center.x) {
450                left_endpoint = left_endpoint.max(block.bottom_right.x);
451            }
452            if block.is_right_of(center.x) {
453                right_endpoint = right_endpoint.min(block.top_left.x);
454            }
455        }
456        if left_endpoint < right_endpoint {
457            self.add_horiz_segment(center.y, left_endpoint, right_endpoint, cost);
458        }
459    }
460    fn seed_vert_channel(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
461        let center: Point = center.into();
462        let cost: Cost = cost.into();
463        let mut top_endpoint = NEG_INFINITY_Y;
464        let mut bottom_endpoint = INFINITY_Y;
465        // Only blocks spanning `center.x` (expanded, leaving a one-unit gutter) can
466        // clip a vertical channel there; the index yields exactly those.
467        for block in self.block_index.spanning_x(center.x, &self.blocks) {
468            if block.spans_y(center.y) {
469                // Seed point is inside a block, so no channel can pass through here.
470                return;
471            }
472            if block.is_above(center.y) {
473                top_endpoint = top_endpoint.max(block.bottom_right.y);
474            }
475            if block.is_below(center.y) {
476                bottom_endpoint = bottom_endpoint.min(block.top_left.y);
477            }
478        }
479        if top_endpoint < bottom_endpoint {
480            self.add_vert_segment(center.x, top_endpoint, bottom_endpoint, cost);
481        }
482    }
483    pub fn seed_channels(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
484        let center: Point = center.into();
485        let cost: Cost = cost.into();
486        self.seed_horiz_channel(center, cost);
487        self.seed_vert_channel(center, cost);
488    }
489    pub fn add_horiz_segment(
490        &mut self,
491        vert: impl Into<CoordY>,
492        left: impl Into<CoordX>,
493        right: impl Into<CoordX>,
494        cost: impl Into<Cost>,
495    ) {
496        let vert: CoordY = vert.into();
497        let left: CoordX = left.into();
498        let right: CoordX = right.into();
499        let cost: Cost = cost.into();
500        if right > left {
501            self.h_segments
502                .entry(vert)
503                .or_default()
504                .push(hseg(left, right, cost));
505            self.dirty = true;
506        }
507    }
508    pub fn add_vert_segment(
509        &mut self,
510        horiz: impl Into<CoordX>,
511        top: impl Into<CoordY>,
512        bottom: impl Into<CoordY>,
513        cost: impl Into<Cost>,
514    ) {
515        let horiz: CoordX = horiz.into();
516        let top: CoordY = top.into();
517        let bottom: CoordY = bottom.into();
518        let cost: Cost = cost.into();
519        if bottom > top {
520            self.v_segments
521                .entry(horiz)
522                .or_default()
523                .push(vseg(top, bottom, cost));
524            self.dirty = true;
525        }
526    }
527    pub fn update(&mut self) {
528        if !self.dirty {
529            return;
530        }
531        // Only real (dirty) rebuilds are timed; the no-op early return above is
532        // the common case and would drown the trace.
533        let _span = tracing::info_span!("router_rebuild").entered();
534        let h_segments = std::mem::take(&mut self.h_segments);
535        for (vert, segments) in h_segments {
536            normalize_collinear_segments(segments, |left, right, cost| {
537                self.add_horiz_segment(vert, left, right, cost);
538            });
539        }
540        let v_segments = std::mem::take(&mut self.v_segments);
541        for (horiz, segments) in v_segments {
542            normalize_collinear_segments(segments, |top, bottom, cost| {
543                self.add_vert_segment(horiz, top, bottom, cost);
544            });
545        }
546        self.nodes = collect_intersections(self.iter_hsegs(), self.iter_vsegs());
547        // Re-segment, but now add segments for each node.
548        let mut h_segments = std::mem::take(&mut self.h_segments);
549        self.nodes.iter().for_each(|&node| {
550            h_segments
551                .entry(node.y)
552                .or_default()
553                .push(hseg(node.x, node.x, COST_ZERO));
554        });
555        for (vert, segments) in h_segments {
556            normalize_collinear_segments(segments, |left, right, cost| {
557                self.add_horiz_segment(vert, left, right, cost);
558            });
559        }
560        let mut v_segments = std::mem::take(&mut self.v_segments);
561        self.nodes.iter().for_each(|&node| {
562            v_segments
563                .entry(node.x)
564                .or_default()
565                .push(vseg(node.y, node.y, COST_ZERO));
566        });
567        for (horiz, segments) in v_segments {
568            normalize_collinear_segments(segments, |top, bottom, cost| {
569                self.add_vert_segment(horiz, top, bottom, cost);
570            });
571        }
572        // The final node set is exactly the endpoints of the now-split segments.
573        // Pass 2 injected a zero-length marker at every crossing (found by the first
574        // `collect_intersections`) and re-normalized, which splits each segment at
575        // those crossings — so every crossing is now a segment endpoint. A second
576        // intersection sweep would only recompute those same points, so skip it.
577        let nodes = self
578            .iter_hsegs()
579            .flat_map(|(y, h_seg)| [point(h_seg.start, y), point(h_seg.end, y)])
580            .chain(
581                self.iter_vsegs()
582                    .flat_map(|(x, v_seg)| [point(x, v_seg.start), point(x, v_seg.end)]),
583            )
584            .collect();
585        self.nodes = nodes;
586        self.rebuild_graph();
587        self.dirty = false;
588    }
589    fn iter_hsegs(&self) -> impl Iterator<Item = (CoordY, HSegment)> + '_ {
590        self.h_segments
591            .iter()
592            .flat_map(|(&y, h_segs)| h_segs.iter().map(move |h_seg| (y, *h_seg)))
593    }
594    fn iter_vsegs(&self) -> impl Iterator<Item = (CoordX, VSegment)> + '_ {
595        self.v_segments
596            .iter()
597            .flat_map(|(&x, v_segs)| v_segs.iter().map(move |v_seg| (x, *v_seg)))
598    }
599    fn rebuild_graph(&mut self) {
600        let mut node_to_index: BTreeMap<Point, petgraph::graph::NodeIndex> = BTreeMap::new();
601        let mut graph = UnGraph::default();
602        for &node in &self.nodes {
603            let index = graph.add_node(node);
604            node_to_index.insert(node, index);
605        }
606        for hseg in self.iter_hsegs() {
607            let y = hseg.0;
608            let h_seg = hseg.1;
609            let start_node = point(h_seg.start, y);
610            let end_node = point(h_seg.end, y);
611            graph.add_edge(
612                node_to_index[&start_node],
613                node_to_index[&end_node],
614                h_seg.cost,
615            );
616        }
617        for vseg in self.iter_vsegs() {
618            let x = vseg.0;
619            let v_seg = vseg.1;
620            let start_node = point(x, v_seg.start);
621            let end_node = point(x, v_seg.end);
622            graph.add_edge(
623                node_to_index[&start_node],
624                node_to_index[&end_node],
625                v_seg.cost,
626            );
627        }
628        self.graph = graph;
629        self.node_to_index = node_to_index;
630    }
631    /// The grid point a node index stands for. Every index the graph hands out
632    /// resolves; a stray one yields the origin rather than taking the editor down.
633    fn point(&self, node: NodeIndex) -> Point {
634        self.graph.node_weight(node).copied().unwrap_or(Point::ZERO)
635    }
636
637    fn successors(&self, state: SearchState) -> Vec<(SearchState, Cost)> {
638        let prev_dir = state.dir;
639        let prev_point = self.point(state.node);
640        let mut north_cost: Option<Cost> = None;
641        let mut south_cost: Option<Cost> = None;
642        let mut east_cost: Option<Cost> = None;
643        let mut west_cost: Option<Cost> = None;
644        // Get the costs to move in the 4 cardinal directions from the current node.
645        for edge in self.graph.edges(state.node) {
646            let neighbor = edge.target();
647            let cost = *edge.weight();
648            let neighbor_point = self.point(neighbor);
649            if neighbor_point.x > prev_point.x {
650                east_cost = Some(cost);
651            } else if neighbor_point.x < prev_point.x {
652                west_cost = Some(cost);
653            } else if neighbor_point.y > prev_point.y {
654                south_cost = Some(cost);
655            } else {
656                north_cost = Some(cost);
657            }
658        }
659        // Calculate the east and west cost as a single cost,
660        // since if we are north/south bound, we should consider
661        // this a crossing.
662        let east_west_crossing_cost = match (east_cost, west_cost) {
663            (Some(east), Some(west)) => east.max(west),
664            _ => COST_ZERO,
665        };
666        let north_south_crossing_cost = match (north_cost, south_cost) {
667            (Some(north), Some(south)) => north.max(south),
668            _ => COST_ZERO,
669        };
670        // Rescan the edges to generate the successors with the correct costs.
671        self.graph
672            .edges(state.node)
673            .map(|edge| {
674                let neighbor = edge.target();
675                let cost = *edge.weight();
676                let neighbor_point = self.point(neighbor);
677                let dir = if neighbor_point.x > prev_point.x {
678                    Direction::East
679                } else if neighbor_point.x < prev_point.x {
680                    Direction::West
681                } else if neighbor_point.y > prev_point.y {
682                    Direction::South
683                } else {
684                    Direction::North
685                };
686                let step_length = neighbor_point.manhattan_distance(prev_point) as f64;
687                let step_cost = turn_cost(prev_dir, dir)
688                    + MOVE_COST * step_length
689                    + cost * step_length
690                    + cross_cost(
691                        prev_dir,
692                        dir,
693                        east_west_crossing_cost,
694                        north_south_crossing_cost,
695                    );
696                (
697                    SearchState {
698                        node: neighbor,
699                        dir: Some(dir),
700                    },
701                    step_cost,
702                )
703            })
704            .collect()
705    }
706    /// Route from `start` to `end`, seeding the search with `incoming` as the
707    /// direction already being travelled (so reversing it out of `start` pays the
708    /// `turn_cost` reversal penalty). Returns the path together with the direction
709    /// of its final segment — the direction of travel arriving at `end` — so a
710    /// caller routing several segments in series can forbid the next one doubling
711    /// back.
712    fn path_find(
713        &mut self,
714        start: impl Into<Point>,
715        end: impl Into<Point>,
716        incoming: Option<Direction>,
717    ) -> Option<(Vec<Point>, Option<Direction>)> {
718        self.update();
719        let start: Point = start.into();
720        let end: Point = end.into();
721        let &start_node = self.node_to_index.get(&start)?;
722        let end_node = self.node_to_index.get(&end)?;
723        let start = SearchState {
724            node: start_node,
725            dir: incoming,
726        };
727        let result = dijkstra(
728            &start,
729            |state| self.successors(*state),
730            |state| state.node == *end_node,
731        );
732        result.map(|(path, _cost)| {
733            let outgoing = path.last().and_then(|state| state.dir);
734            let points = path
735                .into_iter()
736                .map(|state| self.point(state.node))
737                .collect();
738            (points, outgoing)
739        })
740    }
741    pub fn path_find_with_fallback(
742        &mut self,
743        start: impl Into<Point>,
744        end: impl Into<Point>,
745        incoming: Option<Direction>,
746    ) -> (Vec<Point>, Option<Direction>) {
747        let start: Point = start.into();
748        let end: Point = end.into();
749        if let Some(result) = self.path_find(start, end, incoming) {
750            return result;
751        }
752        // Couldn't find a path. so just connect the two points with a horizontal and vertical segment.
753        let path = vec![start, point(end.x, start.y), end];
754        let outgoing = path
755            .windows(2)
756            .rev()
757            .find_map(|w| direction_between(w[0], w[1]))
758            .or(incoming);
759        (path, outgoing)
760    }
761}
762
763// Run a line-sweep style algorithm to collect the intersections.
764// The algorithm works by creating a list of events sorted in x.  Each event
765// is either the start or end of a horizontal segment (using the Enter/Exit events)
766// or a vertical segment (using the Scan event).  The events are sorted by their X-coordinate,
767// and then processed in order.  We maintain a list of active horizontal segments at any given
768// time, and then when we encounter a scan event, we list out all intersections of that vertical
769// segment with the active horizontal segments.
770/// Algorithmic counters for the sweep, compiled only under `cfg(test)` so
771/// production is untouched. Used by the profiling benchmarks to quantify the
772/// inner-loop work vs. the intersections it actually produces.
773#[cfg(test)]
774pub(crate) mod ci_stats {
775    use std::sync::atomic::{AtomicU64, Ordering};
776    pub static CALLS: AtomicU64 = AtomicU64::new(0);
777    pub static EVENTS: AtomicU64 = AtomicU64::new(0);
778    pub static SCANS: AtomicU64 = AtomicU64::new(0);
779    pub static INNER_ITERS: AtomicU64 = AtomicU64::new(0);
780    pub static INTERSECTIONS: AtomicU64 = AtomicU64::new(0);
781    pub fn reset() {
782        for c in [&CALLS, &EVENTS, &SCANS, &INNER_ITERS, &INTERSECTIONS] {
783            c.store(0, Ordering::Relaxed);
784        }
785    }
786    pub fn dump(label: &str) {
787        let g = |c: &AtomicU64| c.load(Ordering::Relaxed);
788        eprintln!(
789            "[{label}] collect_intersections calls={} events={} scans(V)={} \
790             inner_iters={} intersections={} (waste = inner_iters/intersections = {:.1}x)",
791            g(&CALLS),
792            g(&EVENTS),
793            g(&SCANS),
794            g(&INNER_ITERS),
795            g(&INTERSECTIONS),
796            g(&INNER_ITERS) as f64 / (g(&INTERSECTIONS).max(1)) as f64,
797        );
798    }
799}
800
801fn collect_intersections(
802    h_segments: impl IntoIterator<Item = (CoordY, HSegment)>,
803    v_segments: impl IntoIterator<Item = (CoordX, VSegment)>,
804) -> BTreeSet<Point> {
805    let mut events: Vec<Event<CoordX, (CoordY, CoordY)>> = h_segments
806        .into_iter()
807        .flat_map(|(y, h_seg)| {
808            [
809                Event::enter(h_seg.start, (y, y)),
810                Event::exit(h_seg.end, (y, y)),
811            ]
812        })
813        .chain(
814            v_segments
815                .into_iter()
816                .map(|(x, v_seg)| Event::scan(x, (v_seg.start, v_seg.end))),
817        )
818        .collect::<Vec<_>>();
819    events.sort();
820    #[cfg(test)]
821    {
822        use std::sync::atomic::Ordering::Relaxed;
823        ci_stats::CALLS.fetch_add(1, Relaxed);
824        ci_stats::EVENTS.fetch_add(events.len() as u64, Relaxed);
825    }
826    let mut intersections = BTreeSet::new();
827    // Use a map to count active segments at each y-coordinate
828    // This handles segments that touch at boundaries (e.g., one ends at x=10, another starts at x=10)
829    let mut active_h_segments: BTreeMap<CoordY, usize> = BTreeMap::new();
830    for event in events {
831        match event.sense() {
832            EventSense::Enter => {
833                let y = event.cost().0;
834                *active_h_segments.entry(y).or_insert(0) += 1;
835            }
836            EventSense::Exit => {
837                let y = event.cost().0;
838                if let Some(count) = active_h_segments.get_mut(&y) {
839                    *count = count.saturating_sub(1);
840                    if *count == 0 {
841                        active_h_segments.remove(&y);
842                    }
843                }
844            }
845            EventSense::Scan => {
846                let (start, end) = event.cost();
847                // Only the active horizontal segments whose `y` falls in this
848                // vertical segment's span intersect it. `active_h_segments` is
849                // keyed by `y` and only holds entries with a non-zero count, so a
850                // range query yields exactly those — no scanning past the span.
851                let in_span = active_h_segments.range(start..=end);
852                #[cfg(test)]
853                {
854                    use std::sync::atomic::Ordering::Relaxed;
855                    ci_stats::SCANS.fetch_add(1, Relaxed);
856                    ci_stats::INNER_ITERS.fetch_add(in_span.clone().count() as u64, Relaxed);
857                }
858                for (&y, _) in in_span {
859                    intersections.insert(point(event.t(), y));
860                    #[cfg(test)]
861                    ci_stats::INTERSECTIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
862                }
863            }
864        }
865    }
866    intersections
867}
868
869fn normalize_collinear_segments<T: Ord + Copy>(
870    segments: impl IntoIterator<Item = Segment<T>>,
871    mut maker: impl FnMut(T, T, Cost),
872) {
873    let mut events = segments
874        .into_iter()
875        .flat_map(|seg| {
876            [
877                Event::enter(seg.start, seg.cost),
878                Event::exit(seg.end, seg.cost),
879            ]
880        })
881        .collect::<Vec<_>>();
882    // Sort the events by their coordinate, with Enter events before Exit events in case of ties.
883    events.sort();
884    scan_disjoint_segments(events, |start, end, cost| {
885        maker(start, end, cost);
886    });
887}
888
889fn scan_disjoint_segments<T: Ord + Copy>(
890    events: impl IntoIterator<Item = Event<T, Cost>>,
891    mut maker: impl FnMut(T, T, Cost),
892) {
893    let mut events_iter = events.into_iter();
894
895    // Handle the first event to initialize state
896    let Some(first_event) = events_iter.next() else {
897        return;
898    };
899
900    let mut last_t = first_event.t();
901    let mut line_count = first_event.count();
902    let mut current_cost = if first_event.is_enter() {
903        first_event.cost()
904    } else {
905        COST_ZERO - first_event.cost()
906    };
907
908    // Process remaining events
909    for event in events_iter {
910        let t = event.t();
911        // Invariant: line_count > 0 means last_t was assigned in a previous iteration
912        if line_count != 0 {
913            maker(last_t, t, current_cost);
914        }
915        last_t = t;
916        line_count += event.count();
917        current_cost = if event.is_enter() {
918            current_cost + event.cost()
919        } else {
920            current_cost - event.cost()
921        };
922    }
923}
924
925fn interval_overlap<T: Ord + Copy>(a_start: T, a_end: T, b_start: T, b_end: T) -> bool {
926    a_start < b_end && b_start < a_end
927}
928
929#[cfg(test)]
930mod tests {
931
932    use super::*;
933
934    #[test]
935    fn block_axis_index_selects_the_same_blocks_as_a_scan() {
936        use crate::router::block::Block;
937        use crate::router::point::point;
938        use std::collections::BTreeSet;
939        // Distinct x's and distinct y's so each block is identifiable per axis.
940        let blocks = vec![
941            Block {
942                top_left: point(0, 0),
943                bottom_right: point(8, 8),
944            },
945            Block {
946                top_left: point(20, 4),
947                bottom_right: point(30, 12),
948            },
949            Block {
950                top_left: point(-10, -3),
951                bottom_right: point(-2, 40),
952            },
953        ];
954        let index = BlockAxisIndex::build(&blocks);
955        for c in -14..=44 {
956            let y = CoordY::from(c);
957            let from_index: BTreeSet<i32> = index
958                .spanning_y(y, &blocks)
959                .map(|b| b.top_left.x.raw())
960                .collect();
961            let brute: BTreeSet<i32> = blocks
962                .iter()
963                .filter(|b| b.expand_y(1).spans_y(y))
964                .map(|b| b.top_left.x.raw())
965                .collect();
966            assert_eq!(from_index, brute, "spanning_y mismatch at y={c}");
967
968            let x = CoordX::from(c);
969            let from_index_x: BTreeSet<i32> = index
970                .spanning_x(x, &blocks)
971                .map(|b| b.top_left.y.raw())
972                .collect();
973            let brute_x: BTreeSet<i32> = blocks
974                .iter()
975                .filter(|b| b.expand_x(1).spans_x(x))
976                .map(|b| b.top_left.y.raw())
977                .collect();
978            assert_eq!(from_index_x, brute_x, "spanning_x mismatch at x={c}");
979        }
980    }
981
982    macro_rules! hseg {
983        (y=$y:expr, [$(($start:expr => $end:expr, $cost:expr)),* $(,)?]) => {
984            BTreeMap::from([(
985                CoordY::from($y),
986                vec![
987                    $(HSegment {
988                        start: CoordX::from($start),
989                        end: CoordX::from($end),
990                        cost: $cost.into(),
991                    }),*
992                ]
993            )])
994        };
995    }
996
997    macro_rules! vseg {
998        (x=$x:expr, [$(($start:expr => $end:expr, $cost:expr)),* $(,)?]) => {
999            BTreeMap::from([(
1000                CoordX::from($x),
1001                vec![
1002                    $(VSegment {
1003                        start: CoordY::from($start),
1004                        end: CoordY::from($end),
1005                        cost: $cost.into(),
1006                    }),*
1007                ]
1008            )])
1009        };
1010    }
1011
1012    // Brute force algorithm
1013    fn collect_intersections_brute_force(
1014        h_segments: impl IntoIterator<Item = (CoordY, HSegment)>,
1015        v_segments: impl IntoIterator<Item = (CoordX, VSegment)>,
1016    ) -> Vec<Point> {
1017        let mut points = vec![];
1018        let v_segments = v_segments.into_iter().collect::<Vec<_>>();
1019        for (y, hseg) in h_segments {
1020            for (x, vseg) in &v_segments {
1021                if hseg.start <= *x && hseg.end >= *x && vseg.start <= y && vseg.end >= y {
1022                    points.push(point(*x, y));
1023                }
1024            }
1025        }
1026        points
1027    }
1028
1029    #[test]
1030    fn test_vseed() {
1031        let mut router = RouterNGBuilder::default().build();
1032        router.seed_vert_channel(point(0, 0), 1.0);
1033        router.update();
1034        assert_eq!(
1035            router.v_segments,
1036            BTreeMap::from([(CoordX::from(0), vec![vseg(NEG_INFINITY_Y, INFINITY_Y, 1.0)])])
1037        );
1038    }
1039
1040    #[test]
1041    fn test_normalize() {
1042        let mut router = RouterNGBuilder::default().build();
1043        router.add_horiz_segment(0, 0, 10, 1.0);
1044        router.add_horiz_segment(0, 5, 15, 2.0);
1045        router.update();
1046        assert_eq!(
1047            router.h_segments,
1048            hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)])
1049        );
1050    }
1051
1052    #[test]
1053    fn test_normalize_complete_overlap() {
1054        // One segment completely contains another
1055        let mut router = RouterNGBuilder::default().build();
1056        router.add_horiz_segment(0, 0, 20, 1.0);
1057        router.add_horiz_segment(0, 5, 15, 2.0);
1058        router.update();
1059        assert_eq!(
1060            router.h_segments,
1061            hseg!(y=0, [(0=>5, 1.0), (5=>15, 3.0), (15=>20, 1.0)])
1062        );
1063    }
1064
1065    #[test]
1066    fn test_normalize_no_overlap() {
1067        // Segments don't overlap at all
1068        let mut router = RouterNGBuilder::default().build();
1069        router.add_horiz_segment(0, 0, 10, 1.0);
1070        router.add_horiz_segment(0, 20, 30, 2.0);
1071        router.update();
1072        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 1.0), (20=>30, 2.0)]));
1073    }
1074
1075    #[test]
1076    fn test_normalize_adjacent_segments() {
1077        // Segments touch at endpoints but don't overlap
1078        let mut router = RouterNGBuilder::default().build();
1079        router.add_horiz_segment(0, 0, 10, 1.0);
1080        router.add_horiz_segment(0, 10, 20, 2.0);
1081        router.update();
1082        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 1.0), (10=>20, 2.0)]));
1083    }
1084
1085    #[test]
1086    fn test_normalize_triple_overlap() {
1087        // Three segments with various overlaps
1088        let mut router = RouterNGBuilder::default().build();
1089        router.add_horiz_segment(0, 0, 15, 1.0);
1090        router.add_horiz_segment(0, 5, 20, 2.0);
1091        router.add_horiz_segment(0, 10, 25, 3.0);
1092        router.update();
1093        assert_eq!(
1094            router.h_segments,
1095            hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 6.0), (15=>20, 5.0), (20=>25, 3.0)])
1096        );
1097    }
1098
1099    #[test]
1100    fn test_normalize_multiple_rows() {
1101        // Segments on different rows should be handled independently
1102        let mut router = RouterNGBuilder::default().build();
1103        router.add_horiz_segment(0, 0, 10, 1.0);
1104        router.add_horiz_segment(0, 5, 15, 2.0);
1105        router.add_horiz_segment(5, 0, 10, 3.0);
1106        router.add_horiz_segment(5, 5, 15, 4.0);
1107        router.update();
1108
1109        let mut expected = BTreeMap::new();
1110        expected.extend(hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)]));
1111        expected.extend(hseg!(y=5, [(0=>5, 3.0), (5=>10, 7.0), (10=>15, 4.0)]));
1112        assert_eq!(router.h_segments, expected);
1113    }
1114
1115    #[test]
1116    fn test_normalize_negative_coords() {
1117        // Segments in negative coordinate space
1118        let mut router = RouterNGBuilder::default().build();
1119        router.add_horiz_segment(-5, -20, -10, 1.0);
1120        router.add_horiz_segment(-5, -15, -5, 2.0);
1121        router.update();
1122        assert_eq!(
1123            router.h_segments,
1124            hseg!(y=-5, [(-20 => -15, 1.0), (-15 => -10, 3.0), (-10 => -5, 2.0)])
1125        );
1126    }
1127
1128    #[test]
1129    fn test_normalize_vertical_segments() {
1130        // Test vertical segment normalization
1131        let mut router = RouterNGBuilder::default().build();
1132        router.add_vert_segment(0, 0, 10, 1.0);
1133        router.add_vert_segment(0, 5, 15, 2.0);
1134        router.update();
1135        assert_eq!(
1136            router.v_segments,
1137            vseg!(x=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)])
1138        );
1139    }
1140
1141    #[test]
1142    fn test_normalize_identical_segments() {
1143        // Same segment added twice
1144        let mut router = RouterNGBuilder::default().build();
1145        router.add_horiz_segment(0, 0, 10, 1.0);
1146        router.add_horiz_segment(0, 0, 10, 1.0);
1147        router.update();
1148        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 2.0)]));
1149    }
1150
1151    #[test]
1152    fn test_normalize_reverse_order() {
1153        // Segments added in decreasing coordinate order
1154        let mut router = RouterNGBuilder::default().build();
1155        router.add_horiz_segment(0, 20, 30, 1.0);
1156        router.add_horiz_segment(0, 10, 25, 2.0);
1157        router.add_horiz_segment(0, 0, 15, 3.0);
1158        router.update();
1159        assert_eq!(
1160            router.h_segments,
1161            hseg!(y=0, [(0=>10, 3.0), (10=>15, 5.0), (15=>20, 2.0), (20=>25, 3.0), (25=>30, 1.0)])
1162        );
1163    }
1164
1165    // Tests for collect_intersections function
1166
1167    #[test]
1168    fn test_collect_intersections_no_intersections() {
1169        // Horizontal segments with no vertical intersections
1170        let h_segs = vec![
1171            (CoordY::from(0), hseg(0, 10, 1.0)),
1172            (CoordY::from(5), hseg(15, 25, 1.0)),
1173        ];
1174        // Vertical segment outside the y-range of all horizontal segments
1175        let v_segs = vec![(CoordX::from(20), vseg(10, 15, 1.0))];
1176        let intersections = collect_intersections(h_segs, v_segs);
1177        assert_eq!(intersections, BTreeSet::new());
1178    }
1179
1180    #[test]
1181    fn test_collect_intersections_at_boundaries() {
1182        // Intersections at segment boundaries (start/end)
1183        let h_segs = vec![(CoordY::from(5), hseg(0, 10, 1.0))];
1184        // Vertical segment at the start of horizontal segment
1185        let v_segs_start = vec![(CoordX::from(0), vseg(0, 10, 1.0))];
1186        let intersections = collect_intersections(h_segs.clone(), v_segs_start);
1187        assert_eq!(intersections, BTreeSet::from([point(0, 5)]));
1188
1189        // Vertical segment at the end of horizontal segment
1190        let v_segs_end = vec![(CoordX::from(10), vseg(0, 10, 1.0))];
1191        let intersections = collect_intersections(h_segs.clone(), v_segs_end);
1192        assert_eq!(intersections, BTreeSet::from([point(10, 5)]));
1193
1194        // Vertical segment spanning from horizontal's y-coordinate exactly
1195        let v_segs_y_start = vec![(CoordX::from(5), vseg(5, 15, 1.0))];
1196        let intersections = collect_intersections(h_segs.clone(), v_segs_y_start);
1197        assert_eq!(intersections, BTreeSet::from([point(5, 5)]));
1198
1199        // Vertical segment ending at horizontal's y-coordinate exactly
1200        let v_segs_y_end = vec![(CoordX::from(5), vseg(0, 5, 1.0))];
1201        let intersections = collect_intersections(h_segs, v_segs_y_end);
1202        assert_eq!(intersections, BTreeSet::from([point(5, 5)]));
1203    }
1204
1205    #[test]
1206    fn test_collect_intersections_corners() {
1207        // Corner case: vertical start == horizontal start
1208        let h_segs = vec![(CoordY::from(5), hseg(10, 20, 1.0))];
1209        let v_segs = vec![(CoordX::from(10), vseg(5, 15, 1.0))];
1210        let intersections = collect_intersections(h_segs, v_segs);
1211        assert_eq!(intersections, BTreeSet::from([point(10, 5)]));
1212    }
1213
1214    #[test]
1215    fn test_collect_intersections_corner_all_endpoints() {
1216        // All four corners: (h.start, v.start), (h.start, v.end), (h.end, v.start), (h.end, v.end)
1217        let h_seg_y = CoordY::from(10);
1218        let h_segs = vec![(h_seg_y, hseg(5, 15, 1.0))];
1219
1220        // Test (h.start, v.start) corner
1221        let v_segs = vec![(CoordX::from(5), vseg(10, 20, 1.0))];
1222        let intersections = collect_intersections(h_segs.clone(), v_segs);
1223        assert_eq!(intersections, BTreeSet::from([point(5, 10)]));
1224
1225        // Test (h.start, v.end) corner
1226        let v_segs = vec![(CoordX::from(5), vseg(0, 10, 1.0))];
1227        let intersections = collect_intersections(h_segs.clone(), v_segs);
1228        assert_eq!(intersections, BTreeSet::from([point(5, 10)]));
1229
1230        // Test (h.end, v.start) corner
1231        let v_segs = vec![(CoordX::from(15), vseg(10, 20, 1.0))];
1232        let intersections = collect_intersections(h_segs.clone(), v_segs);
1233        assert_eq!(intersections, BTreeSet::from([point(15, 10)]));
1234
1235        // Test (h.end, v.end) corner
1236        let v_segs = vec![(CoordX::from(15), vseg(0, 10, 1.0))];
1237        let intersections = collect_intersections(h_segs, v_segs);
1238        assert_eq!(intersections, BTreeSet::from([point(15, 10)]));
1239    }
1240
1241    #[test]
1242    fn test_collect_intersections_multiple() {
1243        // Multiple intersections from a single vertical segment crossing multiple horizontal segments
1244        let h_segs = vec![
1245            (CoordY::from(5), hseg(0, 20, 1.0)),
1246            (CoordY::from(10), hseg(0, 20, 1.0)),
1247            (CoordY::from(15), hseg(0, 20, 1.0)),
1248        ];
1249        let v_segs = vec![(CoordX::from(10), vseg(0, 20, 1.0))];
1250        let intersections = collect_intersections(h_segs, v_segs);
1251        assert_eq!(
1252            intersections,
1253            BTreeSet::from([point(10, 5), point(10, 10), point(10, 15)])
1254        );
1255    }
1256
1257    #[test]
1258    fn test_collect_intersections_multiple_verticals() {
1259        // Single horizontal segment crossing multiple vertical segments
1260        let h_segs = vec![(CoordY::from(10), hseg(0, 30, 1.0))];
1261        let v_segs = vec![
1262            (CoordX::from(5), vseg(5, 15, 1.0)),
1263            (CoordX::from(15), vseg(5, 15, 1.0)),
1264            (CoordX::from(25), vseg(5, 15, 1.0)),
1265        ];
1266        let intersections = collect_intersections(h_segs, v_segs);
1267        assert_eq!(
1268            intersections,
1269            BTreeSet::from([point(5, 10), point(15, 10), point(25, 10)])
1270        );
1271    }
1272
1273    #[test]
1274    fn test_collect_intersections_vertical_outside_horizontal_y_range() {
1275        // Vertical segment exists in x-range of horizontal but y is outside
1276        let h_segs = vec![(CoordY::from(10), hseg(0, 20, 1.0))];
1277        let v_segs = vec![(CoordX::from(10), vseg(15, 25, 1.0))];
1278        let intersections = collect_intersections(h_segs, v_segs);
1279        assert_eq!(intersections, BTreeSet::new());
1280    }
1281
1282    #[test]
1283    fn test_collect_intersections_empty_inputs() {
1284        // Empty horizontal segments
1285        let intersections =
1286            collect_intersections(vec![], vec![(CoordX::from(5), vseg(0, 10, 1.0))]);
1287        assert_eq!(intersections, BTreeSet::new());
1288
1289        // Empty vertical segments
1290        let intersections =
1291            collect_intersections(vec![(CoordY::from(5), hseg(0, 10, 1.0))], vec![]);
1292        assert_eq!(intersections, BTreeSet::new());
1293
1294        // Both empty
1295        let intersections: BTreeSet<Point> = collect_intersections(
1296            Vec::<(CoordY, HSegment)>::new(),
1297            Vec::<(CoordX, VSegment)>::new(),
1298        );
1299        assert_eq!(intersections, BTreeSet::new());
1300    }
1301
1302    #[test]
1303    fn test_random_segments_line_sweep_matches_brute_force() {
1304        use rand::rngs::StdRng;
1305        use rand::{Rng, SeedableRng};
1306
1307        const NUM_H_SEGMENTS: usize = 1000;
1308        const NUM_V_SEGMENTS: usize = 1000;
1309        const FIELD_SIZE: i32 = 200;
1310
1311        // Use a fixed seed for reproducibility
1312        let mut rng = StdRng::seed_from_u64(42);
1313
1314        let mut router = RouterNGBuilder::default().build();
1315
1316        // Generate random horizontal segments
1317        for _ in 0..NUM_H_SEGMENTS {
1318            let y = rng.random_range(0..FIELD_SIZE);
1319            let x1 = rng.random_range(0..FIELD_SIZE);
1320            let x2 = rng.random_range(0..FIELD_SIZE);
1321            let (start, end) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
1322            // Ensure non-zero length segments
1323            if start < end {
1324                router.add_horiz_segment(y, start, end, rng.random_range(0.1..10.0));
1325            }
1326        }
1327
1328        // Generate random vertical segments
1329        for _ in 0..NUM_V_SEGMENTS {
1330            let x = rng.random_range(0..FIELD_SIZE);
1331            let y1 = rng.random_range(0..FIELD_SIZE);
1332            let y2 = rng.random_range(0..FIELD_SIZE);
1333            let (start, end) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
1334            // Ensure non-zero length segments
1335            if start < end {
1336                router.add_vert_segment(x, start, end, rng.random_range(0.1..10.0));
1337            }
1338        }
1339
1340        // Normalize segments (merge overlapping segments)
1341        let normalize_start = std::time::Instant::now();
1342        router.update();
1343        let normalize_time = normalize_start.elapsed();
1344
1345        println!("Normalization took: {normalize_time:?}");
1346        println!(
1347            "Normalized to {} horizontal segments and {} vertical segments",
1348            router
1349                .h_segments
1350                .values()
1351                .map(std::vec::Vec::len)
1352                .sum::<usize>(),
1353            router
1354                .v_segments
1355                .values()
1356                .map(std::vec::Vec::len)
1357                .sum::<usize>()
1358        );
1359
1360        // Collect intersections using line-sweep algorithm
1361        let line_sweep_start = std::time::Instant::now();
1362        let line_sweep_intersections =
1363            collect_intersections(router.iter_hsegs(), router.iter_vsegs());
1364        let line_sweep_time = line_sweep_start.elapsed();
1365
1366        println!("Line-sweep algorithm took: {line_sweep_time:?}");
1367        println!("Found {} intersections", line_sweep_intersections.len());
1368
1369        // Collect intersections using brute force algorithm
1370        let brute_force_start = std::time::Instant::now();
1371        let brute_force_intersections: BTreeSet<Point> =
1372            collect_intersections_brute_force(router.iter_hsegs(), router.iter_vsegs())
1373                .into_iter()
1374                .collect();
1375        let brute_force_time = brute_force_start.elapsed();
1376
1377        println!("Brute-force algorithm took: {brute_force_time:?}");
1378
1379        let speedup = brute_force_time.as_secs_f64() / line_sweep_time.as_secs_f64();
1380        println!("Line-sweep is {speedup:.2}x faster than brute-force");
1381
1382        // Compare results
1383        assert_eq!(
1384            line_sweep_intersections.len(),
1385            brute_force_intersections.len(),
1386            "Number of intersections differs: line-sweep found {}, brute-force found {}",
1387            line_sweep_intersections.len(),
1388            brute_force_intersections.len()
1389        );
1390
1391        assert_eq!(
1392            line_sweep_intersections, brute_force_intersections,
1393            "Intersection sets differ between line-sweep and brute-force algorithms"
1394        );
1395    }
1396
1397    #[test]
1398    fn incoming_direction_prevents_double_back_out_of_waypoint() {
1399        // Four free channels (no blocks) form a 2x2 grid with corners at
1400        // (0,0), (10,0), (0,-10), (10,-10). The target sits north-east of the
1401        // start, reachable equally cheaply by "east then north" or "north then
1402        // east" — the tie is broken only by the incoming direction.
1403        let mut router = RouterNGBuilder::default().build();
1404        router.seed_channels(point(0, 0), 0.0);
1405        router.seed_channels(point(10, -10), 0.0);
1406
1407        // Arriving at the start heading south, the route must turn east first:
1408        // leaving north would reverse back over the incoming wire (the kink) and
1409        // is forbidden by the reversal penalty.
1410        let (path, outgoing) =
1411            router.path_find_with_fallback(point(0, 0), point(10, -10), Some(Direction::South));
1412
1413        assert!(path.len() >= 2, "expected a real path, got {path:?}");
1414        assert!(
1415            path[1].x > path[0].x,
1416            "router doubled back instead of turning east first: {path:?}"
1417        );
1418        // Its final segment climbs north into the target — the direction threaded
1419        // to the next segment.
1420        assert_eq!(outgoing, Some(Direction::North));
1421    }
1422}