Skip to main content

blockworx_router/
lib.rs

1//! The wire router: an orthogonal, channel-based path finder over the same
2//! integer lattice the document stores geometry on.
3//!
4//! Blocked rectangles and routing channels are declared into a
5//! [`RouterNGBuilder`], which builds the channel graph once and freezes it into
6//! a [`ClosedRouter`]. Routing a leg then only reweights existing edges, so a
7//! whole frame's routes share one graph.
8//!
9//! The lattice is [`blockworx_geom::grid`]'s: a [`point::Point`] converts to and
10//! from [`blockworx_geom::Pos2`] through the same rounding the document's
11//! grid↔world bridge uses, and directly to and from
12//! [`blockworx_doc::geometry::GridPoint`], which is already lattice-aligned.
13
14use rustc_hash::FxHashMap;
15use std::collections::{BTreeMap, BTreeSet};
16
17use blockworx_geom::Pos2;
18use pathfinding::directed::dijkstra::dijkstra;
19use petgraph::{
20    graph::{NodeIndex, UnGraph},
21    visit::EdgeRef,
22};
23
24pub mod block;
25pub mod channel;
26#[cfg(any(test, feature = "test-support"))]
27pub mod ci_stats;
28pub mod coord;
29pub mod cost;
30pub mod event;
31pub mod point;
32pub mod segment;
33pub mod turtle;
34
35use crate::{
36    block::{Block, ROUTE_GUTTER},
37    channel::{Channel, ChannelOrientation, h_channel, v_channel},
38    coord::{CoordX, CoordY, INFINITY_X, INFINITY_Y, NEG_INFINITY_X, NEG_INFINITY_Y},
39    cost::{COST_ZERO, Cost},
40    event::{Event, EventSense},
41    point::{Point, point},
42    segment::{HSegment, Segment, VSegment, hseg, vseg},
43    turtle::{Mark, Turtle},
44};
45
46/// Whether a leg found a path through the lattice, or fell back to an L that
47/// ignores it — which may run straight through a block.
48#[derive(Clone, Copy, PartialEq, Eq, Debug)]
49pub enum Resolution {
50    Routed,
51    Fallback,
52}
53
54impl Resolution {
55    /// A wire of several legs is routed only if every leg is.
56    #[must_use]
57    pub fn and(self, other: Self) -> Self {
58        match (self, other) {
59            (Self::Routed, Self::Routed) => Self::Routed,
60            _ => Self::Fallback,
61        }
62    }
63}
64
65/// One leg as routed: its points, the direction it arrives in, and whether the
66/// lattice held a path for it.
67#[derive(Clone, Debug)]
68pub struct Leg {
69    pub path: Vec<Point>,
70    pub outgoing: Option<Direction>,
71    pub resolution: Resolution,
72}
73
74#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
75pub enum Direction {
76    North,
77    South,
78    East,
79    West,
80}
81
82impl Direction {
83    fn opposite(self) -> Self {
84        match self {
85            Direction::North => Direction::South,
86            Direction::South => Direction::North,
87            Direction::East => Direction::West,
88            Direction::West => Direction::East,
89        }
90    }
91}
92
93/// The cardinal direction of travel from `from` to `to`, or `None` if the points
94/// coincide. Greater `y` is `South`, matching the successor walk the search
95/// itself uses.
96pub fn direction_between(from: Point, to: Point) -> Option<Direction> {
97    if to.x > from.x {
98        Some(Direction::East)
99    } else if to.x < from.x {
100        Some(Direction::West)
101    } else if to.y > from.y {
102        Some(Direction::South)
103    } else if to.y < from.y {
104        Some(Direction::North)
105    } else {
106        None
107    }
108}
109
110const TURN_COST: Cost = Cost::new(25.0);
111const MOVE_COST: Cost = Cost::new(1.0);
112pub const WIRE_COST: Cost = Cost::new(10.0);
113
114fn cross_cost(
115    from: Option<Direction>,
116    to: Direction,
117    cost_to_cross_east_west: Cost,
118    cost_to_cross_north_south: Cost,
119) -> Cost {
120    if let Some(from_dir) = from {
121        match (from_dir, to) {
122            (Direction::North, Direction::South) | (Direction::South, Direction::North) => {
123                cost_to_cross_east_west
124            }
125            (Direction::East, Direction::West) | (Direction::West, Direction::East) => {
126                cost_to_cross_north_south
127            }
128            _ => COST_ZERO,
129        }
130    } else {
131        COST_ZERO
132    }
133}
134
135fn turn_cost(from: Option<Direction>, to: Direction) -> Cost {
136    if let Some(from_dir) = from {
137        if from_dir == to {
138            COST_ZERO
139        } else if to == from_dir.opposite() {
140            TURN_COST * 100.0
141        } else {
142            TURN_COST
143        }
144    } else {
145        COST_ZERO
146    }
147}
148
149#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)]
150struct SearchState {
151    node: NodeIndex,
152    dir: Option<Direction>,
153}
154
155/// The rectangle a lattice covers.
156///
157/// Channels are clipped to it exactly as they are to blocks, blocks outside it
158/// are ignored, and a seed point outside it seeds nothing. [`Self::UNBOUNDED`]
159/// is the whole plane, so a bounded lattice and a whole-sheet one are the same
160/// code path with different numbers in it — there is no "is this bounded"
161/// branch to get wrong.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct Bounds {
164    left: CoordX,
165    right: CoordX,
166    top: CoordY,
167    bottom: CoordY,
168}
169
170impl Default for Bounds {
171    fn default() -> Self {
172        Self::UNBOUNDED
173    }
174}
175
176impl Bounds {
177    /// The whole plane.
178    pub const UNBOUNDED: Self = Self {
179        left: NEG_INFINITY_X,
180        right: INFINITY_X,
181        top: NEG_INFINITY_Y,
182        bottom: INFINITY_Y,
183    };
184
185    /// The rectangle between two corners, in either order.
186    #[must_use]
187    pub fn between(a: impl Into<Point>, b: impl Into<Point>) -> Self {
188        let (a, b) = (a.into(), b.into());
189        Self {
190            left: a.x.min(b.x),
191            right: a.x.max(b.x),
192            top: a.y.min(b.y),
193            bottom: a.y.max(b.y),
194        }
195    }
196
197    /// Is this point inside?
198    #[must_use]
199    pub fn holds(self, p: impl Into<Point>) -> bool {
200        let p: Point = p.into();
201        (self.left..=self.right).contains(&p.x) && (self.top..=self.bottom).contains(&p.y)
202    }
203
204    /// Does this block reach inside? A block straddling the edge obstructs
205    /// within the bounds too, so it is kept; one entirely outside can clip
206    /// nothing inside and is dropped.
207    #[must_use]
208    fn reaches(self, top_left: Point, bottom_right: Point) -> bool {
209        top_left.x <= self.right
210            && bottom_right.x >= self.left
211            && top_left.y <= self.bottom
212            && bottom_right.y >= self.top
213    }
214}
215
216#[derive(Debug, Clone, Default)]
217pub struct RouterNGBuilder {
218    /// What the lattice covers.
219    bounds: Bounds,
220    /// The blocking rectangles
221    blocks: Vec<Block>,
222    /// The routing channels
223    channels: Vec<Channel>,
224    /// Points at which [`Self::build_closed`] seeds a full (H+V) channel — route
225    /// endpoints and waypoints. Ignored by the test-only `build`.
226    seed_points: Vec<Point>,
227}
228
229impl RouterNGBuilder {
230    /// Cover only `bounds`. The default is [`Bounds::UNBOUNDED`].
231    #[must_use]
232    pub fn within(mut self, bounds: Bounds) -> Self {
233        self.bounds = bounds;
234        self
235    }
236
237    pub fn add_h_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
238        self.channels.push(h_channel(seed, cost));
239    }
240    /// Register a point (a route endpoint or waypoint) at which `build_closed`
241    /// seeds a full channel, so the closed graph already contains the nodes every
242    /// route needs — no geometry is added once the graph is closed.
243    pub fn add_seed_point(&mut self, p: impl Into<Point>) {
244        self.seed_points.push(p.into());
245    }
246    pub fn add_v_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
247        self.channels.push(v_channel(seed, cost));
248    }
249    fn add_routing_moat(
250        &mut self,
251        top_left: Point,
252        bottom_right: Point,
253        distance: i32,
254        cost: Cost,
255    ) {
256        let min_x = top_left.x.min(bottom_right.x);
257        let max_x = top_left.x.max(bottom_right.x);
258        let min_y = top_left.y.min(bottom_right.y);
259        let max_y = top_left.y.max(bottom_right.y);
260        self.add_v_channel(point(min_x - distance - 2, min_y), cost);
261        self.add_v_channel(point(min_x - distance - 2, max_y), cost);
262        self.add_v_channel(point(max_x + distance + 2, min_y), cost);
263        self.add_v_channel(point(max_x + distance + 2, max_y), cost);
264        self.add_h_channel(point(min_x, min_y - distance - 2), cost);
265        self.add_h_channel(point(max_x, min_y - distance - 2), cost);
266        self.add_h_channel(point(min_x, max_y + distance + 2), cost);
267        self.add_h_channel(point(max_x, max_y + distance + 2), cost);
268    }
269    pub fn add_block(&mut self, top_left: impl Into<Point>, bottom_right: impl Into<Point>) {
270        let top_left: Point = top_left.into();
271        let bottom_right: Point = bottom_right.into();
272        let min_x = top_left.x.min(bottom_right.x);
273        let max_x = top_left.x.max(bottom_right.x);
274        let min_y = top_left.y.min(bottom_right.y);
275        let max_y = top_left.y.max(bottom_right.y);
276        let block = Block {
277            top_left: point(min_x, min_y),
278            bottom_right: point(max_x, max_y),
279        };
280        if !self.bounds.reaches(block.top_left, block.bottom_right) {
281            return;
282        }
283        self.blocks.push(block);
284        // Add the routing channels around the blocked rectangle.
285        for moat_lane in 0..crate::block::MOAT_LANES {
286            let cost = if moat_lane == 0 {
287                Cost::new(0.2)
288            } else {
289                Cost::new(0.1)
290            };
291            self.add_routing_moat(top_left, bottom_right, moat_lane, cost);
292        }
293    }
294    /// Construct a dirty router with all channels seeded but not yet built into a
295    /// graph. Shared by [`Self::build_closed`] and the test-only `build`.
296    fn seed_channels_into_router(&self) -> RouterNG {
297        let block_index = BlockAxisIndex::build(&self.blocks);
298        let mut router = RouterNG {
299            bounds: self.bounds,
300            blocks: self.blocks.clone(),
301            block_index,
302            h_segments: BTreeMap::new(),
303            v_segments: BTreeMap::new(),
304            nodes: BTreeSet::new(),
305            graph: UnGraph::default(),
306            node_to_index: FxHashMap::default(),
307            dirty: true,
308            requested: Requests {
309                channels: self.channels.clone(),
310                seed_points: Vec::new(),
311            },
312        };
313        for channel in &self.channels {
314            match channel.orientation {
315                ChannelOrientation::Horizontal => {
316                    router.seed_horiz_channel(channel.seed, channel.cost);
317                }
318                ChannelOrientation::Vertical => {
319                    router.seed_vert_channel(channel.seed, channel.cost);
320                }
321            }
322        }
323        router
324    }
325    /// Build the mutable [`RouterNG`] directly. Only the router's own unit tests
326    /// construct one this way now; production code uses [`Self::build_closed`].
327    #[cfg(test)]
328    pub fn build(self) -> RouterNG {
329        let mut router = self.seed_channels_into_router();
330        router.update();
331        router
332    }
333    /// Build a [`ClosedRouter`]: seed all channels AND the endpoint/waypoint
334    /// channels registered via [`Self::add_seed_point`], build the graph once,
335    /// then freeze the geometry. Routing on the result only mutates edge weights.
336    pub fn build_closed(self) -> ClosedRouter {
337        let mut router = self.seed_channels_into_router();
338        for &p in &self.seed_points {
339            router.seed_channels(p, COST_ZERO);
340        }
341        router.requested.seed_points.clone_from(&self.seed_points);
342        router.update();
343        ClosedRouter { inner: router }
344    }
345}
346
347/// A [`RouterNG`] whose geometry has been built and frozen: it exposes routing
348/// and in-place edge-weight mutation, but no method to add segments/channels or
349/// rebuild the graph. Occupancy of a completed route is applied by adding
350/// [`WIRE_COST`] to the existing graph edges the wire covers, so no rebuild is
351/// needed (see Finding 1 in TUNING.md).
352#[derive(Debug, Clone)]
353pub struct ClosedRouter {
354    inner: RouterNG,
355}
356
357impl ClosedRouter {
358    /// Add geometry to a closed lattice and close it again — how a foreground
359    /// is replayed onto a clone of a cached background.
360    ///
361    /// **Insertion only.** Nothing is ever taken out: a background is *built
362    /// from* the complement of the foreground, and the foreground is added to a
363    /// clone that is thrown away afterwards. So there is no inverse operation
364    /// to get wrong — no provenance to refcount, no cost to subtract, no
365    /// crossing node to heal.
366    ///
367    /// The lattice is re-derived from the requests rather than patched, because
368    /// a channel is clipped by the blocks present when it is seeded: a block
369    /// added afterwards cannot be applied to the segments that came out, and
370    /// splitting them around it would invent a channel on its far side that a
371    /// whole build would never have produced.
372    #[must_use]
373    pub fn extended(mut self, add: impl FnOnce(&mut Opening<'_>)) -> Self {
374        let mut opening = Opening {
375            router: &mut self.inner,
376        };
377        add(&mut opening);
378        self.inner.reseed();
379        self.inner.update();
380        self
381    }
382
383    /// Route one leg from `start` to `end`, threading `incoming` (the direction
384    /// already travelled) so the leg can't double back. Read-only w.r.t.
385    /// geometry — the graph is already closed, so the internal `update()` is a
386    /// no-op. Returns the path points, the outgoing direction, and whether the
387    /// lattice held a path at all.
388    pub fn route_leg(&mut self, start: Point, end: Point, incoming: Option<Direction>) -> Leg {
389        self.inner.path_find_with_fallback(start, end, incoming)
390    }
391
392    /// Add `cost` to every graph edge along the (axis-aligned) `path`, leg by leg.
393    pub fn bump_leg(&mut self, path: &[Point], cost: Cost) {
394        for w in path.windows(2) {
395            self.add_wire_cost(w[0], w[1], cost);
396        }
397    }
398
399    /// Occupy the axis-aligned wire `a → b` by adding `cost` to each of the
400    /// node-to-node graph edges it covers. Walks adjacency in the travel
401    /// direction (edges are split at every node, so each hop is one graph edge).
402    /// Never changes topology or sets `dirty`, so the graph stays closed and
403    /// `successors` reads the new weights live on the next route.
404    pub fn add_wire_cost(&mut self, a: Point, b: Point, cost: Cost) {
405        let Some(dir) = direction_between(a, b) else {
406            return;
407        };
408        let Some(&target) = self.inner.node_to_index.get(&b) else {
409            return;
410        };
411        let Some(&start) = self.inner.node_to_index.get(&a) else {
412            return;
413        };
414        let mut cur = start;
415        let mut cur_pt = a;
416        // Bounded by the number of nodes; the guard only trips on a malformed
417        // (disconnected) graph, in which case we stop rather than loop forever.
418        for _ in 0..self.inner.node_to_index.len() {
419            if cur == target {
420                break;
421            }
422            let step = self.inner.graph.edges(cur).find_map(|e| {
423                let np = self.inner.point(e.target());
424                (direction_between(cur_pt, np) == Some(dir)).then_some((e.id(), e.target(), np))
425            });
426            let Some((edge_id, next, next_pt)) = step else {
427                break;
428            };
429            if let Some(w) = self.inner.graph.edge_weight_mut(edge_id) {
430                *w += cost;
431            }
432            cur = next;
433            cur_pt = next_pt;
434        }
435    }
436
437    /// Whether the axis-aligned wire `a → b` crosses any blocked rectangle.
438    pub fn is_wire_blocked(&self, a: Point, b: Point) -> bool {
439        self.inner
440            .blocks
441            .iter()
442            .any(|block| block.intersects_edge(a, b))
443    }
444
445    /// Whether the straight wire `a → b` hugs any block (runs within the routing
446    /// gutter alongside an edge). Complements [`Self::is_wire_blocked`]: together
447    /// they decide whether a straight leg may be taken verbatim or must route around.
448    pub fn wire_hugs_block(&self, a: Point, b: Point) -> bool {
449        self.inner
450            .blocks
451            .iter()
452            .any(|blk| blk.hugs_wire(a, b, ROUTE_GUTTER))
453    }
454
455    pub fn is_accessible(&self, test: impl Into<Point>) -> bool {
456        self.inner.is_accessible(test)
457    }
458
459    pub fn debug_marks(&self) -> Vec<Mark> {
460        self.inner.debug_marks()
461    }
462
463    /// The lattice as a value that can be compared: every node, and every edge
464    /// with its weight, in a canonical order. Two routers that fingerprint the
465    /// same will route the same, whatever order they were assembled in — which
466    /// is the property a cached background plus a replayed foreground has to
467    /// hold against a whole build.
468    #[must_use]
469    pub fn fingerprint(&self) -> Fingerprint {
470        let mut edges: Vec<(Point, Point, Cost)> = self
471            .inner
472            .graph
473            .edge_indices()
474            .filter_map(|e| {
475                let (a, b) = self.inner.graph.edge_endpoints(e)?;
476                let (a, b) = (self.inner.point(a), self.inner.point(b));
477                let (lo, hi) = if (a.x, a.y) <= (b.x, b.y) {
478                    (a, b)
479                } else {
480                    (b, a)
481                };
482                Some((lo, hi, *self.inner.graph.edge_weight(e)?))
483            })
484            .collect();
485        edges.sort_unstable_by_key(|&(a, b, cost)| ((a.x, a.y), (b.x, b.y), cost));
486        Fingerprint {
487            nodes: self.inner.nodes.iter().copied().collect(),
488            edges,
489        }
490    }
491}
492
493/// A lattice in a form two routers can be compared by. See
494/// [`ClosedRouter::fingerprint`].
495#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct Fingerprint {
497    nodes: Vec<Point>,
498    edges: Vec<(Point, Point, Cost)>,
499}
500
501impl Fingerprint {
502    /// Every node, for a test asking where the lattice reaches.
503    #[must_use]
504    pub fn points(&self) -> &[Point] {
505        &self.nodes
506    }
507
508    /// How many nodes the lattice holds — its size, for a measurement.
509    #[must_use]
510    pub fn nodes(&self) -> usize {
511        self.nodes.len()
512    }
513
514    /// How the two differ, for a test that needs to say more than "not equal".
515    #[must_use]
516    pub fn difference(&self, other: &Self) -> String {
517        let missing_nodes = self
518            .nodes
519            .iter()
520            .filter(|n| !other.nodes.contains(n))
521            .count();
522        let extra_nodes = other
523            .nodes
524            .iter()
525            .filter(|n| !self.nodes.contains(n))
526            .count();
527        format!(
528            "nodes {} vs {} ({missing_nodes} missing, {extra_nodes} extra), edges {} vs {}",
529            self.nodes.len(),
530            other.nodes.len(),
531            self.edges.len(),
532            other.edges.len(),
533        )
534    }
535}
536
537/// A closed lattice being added to. Mirrors [`RouterNGBuilder`]'s vocabulary,
538/// because what it records are the same requests.
539pub struct Opening<'a> {
540    router: &'a mut RouterNG,
541}
542
543impl Opening<'_> {
544    /// Register a point at which a full (H+V) channel is seeded — a route
545    /// endpoint or waypoint.
546    pub fn add_seed_point(&mut self, p: impl Into<Point>) {
547        self.router.requested.seed_points.push(p.into());
548    }
549
550    pub fn add_h_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
551        self.router
552            .requested
553            .channels
554            .push(channel::h_channel(seed, cost));
555    }
556
557    pub fn add_v_channel(&mut self, seed: impl Into<Point>, cost: impl Into<Cost>) {
558        self.router
559            .requested
560            .channels
561            .push(channel::v_channel(seed, cost));
562    }
563
564    /// Add a blocking rectangle and the moat channels around it, exactly as
565    /// [`RouterNGBuilder::add_block`] does — the one addition that also
566    /// *clips*, which is why the lattice is re-seeded rather than patched.
567    pub fn add_block(&mut self, top_left: impl Into<Point>, bottom_right: impl Into<Point>) {
568        let mut builder = RouterNGBuilder::default();
569        builder.add_block(top_left, bottom_right);
570        self.router.blocks.extend(builder.blocks.iter().copied());
571        self.router.requested.channels.extend(builder.channels);
572        self.router.block_index = BlockAxisIndex::build(&self.router.blocks);
573    }
574}
575
576/// A per-axis index of the (immutable) block set, so channel seeding iterates only
577/// the blocks that intersect a query coordinate instead of scanning them all. Built
578/// once when the closed geometry is assembled. `by_y[y]` lists the blocks whose
579/// vertical extent (expanded by one, matching [`RouterNG::seed_horiz_channel`])
580/// contains `y`; `by_x[x]` the blocks whose horizontal extent (expanded by one)
581/// contains `x`. Same coordinate-keyed `BTreeMap<Coord, Vec<_>>` shape as the
582/// segment maps.
583#[derive(Debug, Clone, Default)]
584struct BlockAxisIndex {
585    by_y: BTreeMap<CoordY, Vec<usize>>,
586    by_x: BTreeMap<CoordX, Vec<usize>>,
587}
588
589impl BlockAxisIndex {
590    fn build(blocks: &[Block]) -> Self {
591        let mut by_y: BTreeMap<CoordY, Vec<usize>> = BTreeMap::new();
592        let mut by_x: BTreeMap<CoordX, Vec<usize>> = BTreeMap::new();
593        for (i, block) in blocks.iter().enumerate() {
594            let ey = block.expand_y(1);
595            for y in ey.top_left.y.raw()..=ey.bottom_right.y.raw() {
596                by_y.entry(CoordY::from(y)).or_default().push(i);
597            }
598            let ex = block.expand_x(1);
599            for x in ex.top_left.x.raw()..=ex.bottom_right.x.raw() {
600                by_x.entry(CoordX::from(x)).or_default().push(i);
601            }
602        }
603        Self { by_y, by_x }
604    }
605
606    /// The blocks whose one-expanded vertical extent contains `y`.
607    fn spanning_y<'a>(&'a self, y: CoordY, blocks: &'a [Block]) -> impl Iterator<Item = &'a Block> {
608        self.by_y.get(&y).into_iter().flatten().map(|&i| &blocks[i])
609    }
610
611    /// The blocks whose one-expanded horizontal extent contains `x`.
612    fn spanning_x<'a>(&'a self, x: CoordX, blocks: &'a [Block]) -> impl Iterator<Item = &'a Block> {
613        self.by_x.get(&x).into_iter().flatten().map(|&i| &blocks[i])
614    }
615}
616
617#[derive(Debug, Clone)]
618pub struct RouterNG {
619    /// What this lattice covers. Channels are clipped to it.
620    bounds: Bounds,
621    /// The blocking rectangles - not mutable
622    blocks: Vec<Block>,
623    /// Per-axis index of `blocks` for channel seeding (built from `blocks`).
624    block_index: BlockAxisIndex,
625    /// Horizontal segments, keyed by their vertical coordinate
626    h_segments: BTreeMap<CoordY, Vec<HSegment>>,
627    /// Vertical segments, keyed by their horizontal coordinate
628    v_segments: BTreeMap<CoordX, Vec<VSegment>>,
629    /// Nodes: the intersection points of the segments
630    nodes: BTreeSet<Point>,
631    /// The graph to be used for pathfinding, built from the segments and nodes
632    graph: UnGraph<Point, Cost>,
633    /// A map from node to index in the graph, for quick lookup
634    node_to_index: FxHashMap<Point, petgraph::graph::NodeIndex>,
635    /// Dirty flag that indicates `h_segments` or `v_segments` have been modified and the graph needs to be rebuilt.
636    dirty: bool,
637    /// The channels and seed points this lattice was asked for, kept because a
638    /// segment does not remember where its channel was seeded. Clipping is by
639    /// blocks *at seeding time* — a channel becomes the gap its seed point
640    /// falls in — so a block added later cannot be applied to the segments that
641    /// came out; the requests have to be seeded again against the new block
642    /// set. See [`ClosedRouter::extended`].
643    requested: Requests,
644}
645
646/// What a lattice was asked for, as asked rather than as resolved.
647#[derive(Debug, Clone, Default)]
648struct Requests {
649    channels: Vec<Channel>,
650    seed_points: Vec<Point>,
651}
652
653impl RouterNG {
654    /// The channel graph drawn as debug marks.
655    ///
656    /// # Panics
657    /// If the graph has unbuilt segments: what the marks show would not be what
658    /// a route would be found on.
659    pub fn debug_marks(&self) -> Vec<Mark> {
660        assert!(
661            !self.dirty,
662            "Cannot generate debug marks when the graph is dirty"
663        );
664        let mut turtle = Turtle::default();
665        for node in self.graph.node_indices() {
666            let pos = self.point(node);
667            turtle.move_to(pos.into());
668            turtle.circle(blockworx_geom::WorldPx::new(3.0));
669            // To make the edges more visible, we add a gap at the beginning and
670            // end of the edge line, so that it looks like this * ---- * rather than this *------------------*
671            for edge in self.graph.edges(node) {
672                let target = edge.target();
673                let edge_weight = edge.weight();
674                let target_pos = self.point(target);
675
676                // Calculate direction and distance
677                let start_pos: Pos2 = pos.into();
678                let end_pos: Pos2 = target_pos.into();
679                let dx = end_pos.x - start_pos.x;
680                let dy = end_pos.y - start_pos.y;
681                let distance = (dx * dx + dy * dy).sqrt();
682
683                // Skip very short edges
684                if distance < 8.0 {
685                    continue;
686                }
687
688                // Create 4-pixel gap at each end
689                let gap = 4.0;
690                let gap_ratio = gap / distance;
691
692                // Start point with gap from the node
693                let line_start =
694                    Pos2::new(start_pos.x + dx * gap_ratio, start_pos.y + dy * gap_ratio);
695
696                // End point with gap before the target
697                let line_end = Pos2::new(end_pos.x - dx * gap_ratio, end_pos.y - dy * gap_ratio);
698
699                turtle.move_to(line_start);
700                turtle.line_to(line_end);
701                let mid_point = line_start + (line_end - line_start) / 2.0;
702                let weight: f64 = (*edge_weight).into();
703                turtle.label(mid_point, weight as f32);
704            }
705        }
706        turtle.compile()
707    }
708    pub fn is_accessible(&self, test: impl Into<Point>) -> bool {
709        let test: Point = test.into();
710        !self.blocks.iter().any(|block| block.contains(test))
711    }
712    fn seed_horiz_channel(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
713        let center: Point = center.into();
714        let cost: Cost = cost.into();
715        if !self.bounds.holds(center) {
716            return;
717        }
718        let mut left_endpoint = self.bounds.left;
719        let mut right_endpoint = self.bounds.right;
720        // Only blocks spanning `center.y` can clip a horizontal channel there, and
721        // the index yields exactly those.
722        for block in self.block_index.spanning_y(center.y, &self.blocks) {
723            let block = block.expand_x(1).expand_y(1);
724            if block.spans_x(center.x) {
725                // Seed point is inside a block, so no channel can pass through here.
726                return;
727            }
728            if block.is_left_of(center.x) {
729                left_endpoint = left_endpoint.max(block.bottom_right.x);
730            }
731            if block.is_right_of(center.x) {
732                right_endpoint = right_endpoint.min(block.top_left.x);
733            }
734        }
735        if left_endpoint < right_endpoint {
736            self.add_horiz_segment(center.y, left_endpoint, right_endpoint, cost);
737        }
738    }
739    fn seed_vert_channel(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
740        let center: Point = center.into();
741        let cost: Cost = cost.into();
742        if !self.bounds.holds(center) {
743            return;
744        }
745        let mut top_endpoint = self.bounds.top;
746        let mut bottom_endpoint = self.bounds.bottom;
747        // Only blocks spanning `center.x` (expanded, leaving a one-unit gutter) can
748        // clip a vertical channel there; the index yields exactly those.
749        for block in self.block_index.spanning_x(center.x, &self.blocks) {
750            if block.spans_y(center.y) {
751                // Seed point is inside a block, so no channel can pass through here.
752                return;
753            }
754            if block.is_above(center.y) {
755                top_endpoint = top_endpoint.max(block.bottom_right.y);
756            }
757            if block.is_below(center.y) {
758                bottom_endpoint = bottom_endpoint.min(block.top_left.y);
759            }
760        }
761        if top_endpoint < bottom_endpoint {
762            self.add_vert_segment(center.x, top_endpoint, bottom_endpoint, cost);
763        }
764    }
765    pub fn seed_channels(&mut self, center: impl Into<Point>, cost: impl Into<Cost>) {
766        let center: Point = center.into();
767        let cost: Cost = cost.into();
768        self.seed_horiz_channel(center, cost);
769        self.seed_vert_channel(center, cost);
770    }
771    pub fn add_horiz_segment(
772        &mut self,
773        vert: impl Into<CoordY>,
774        left: impl Into<CoordX>,
775        right: impl Into<CoordX>,
776        cost: impl Into<Cost>,
777    ) {
778        let vert: CoordY = vert.into();
779        let left: CoordX = left.into();
780        let right: CoordX = right.into();
781        let cost: Cost = cost.into();
782        if right > left {
783            self.h_segments
784                .entry(vert)
785                .or_default()
786                .push(hseg(left, right, cost));
787            self.dirty = true;
788        }
789    }
790    pub fn add_vert_segment(
791        &mut self,
792        horiz: impl Into<CoordX>,
793        top: impl Into<CoordY>,
794        bottom: impl Into<CoordY>,
795        cost: impl Into<Cost>,
796    ) {
797        let horiz: CoordX = horiz.into();
798        let top: CoordY = top.into();
799        let bottom: CoordY = bottom.into();
800        let cost: Cost = cost.into();
801        if bottom > top {
802            self.v_segments
803                .entry(horiz)
804                .or_default()
805                .push(vseg(top, bottom, cost));
806            self.dirty = true;
807        }
808    }
809    /// Throw the derived segments away and seed them again from the requests,
810    /// against the current block set. The only way to apply a block that
811    /// arrived after the channels it clips.
812    fn reseed(&mut self) {
813        self.h_segments = BTreeMap::new();
814        self.v_segments = BTreeMap::new();
815        let requested = std::mem::take(&mut self.requested);
816        for channel in &requested.channels {
817            match channel.orientation {
818                ChannelOrientation::Horizontal => {
819                    self.seed_horiz_channel(channel.seed, channel.cost);
820                }
821                ChannelOrientation::Vertical => {
822                    self.seed_vert_channel(channel.seed, channel.cost);
823                }
824            }
825        }
826        for &p in &requested.seed_points {
827            self.seed_channels(p, COST_ZERO);
828        }
829        self.requested = requested;
830        self.dirty = true;
831    }
832
833    pub fn update(&mut self) {
834        if !self.dirty {
835            return;
836        }
837        // Only real (dirty) rebuilds are timed; the no-op early return above is
838        // the common case and would drown the trace.
839        let _span = tracing::info_span!("router_rebuild").entered();
840        {
841            let _s = tracing::info_span!(
842                "normalize",
843                h = self.h_segments.len(),
844                v = self.v_segments.len()
845            )
846            .entered();
847            let h_segments = std::mem::take(&mut self.h_segments);
848            for (vert, segments) in h_segments {
849                normalize_collinear_segments(segments, |left, right, cost| {
850                    self.add_horiz_segment(vert, left, right, cost);
851                });
852            }
853            let v_segments = std::mem::take(&mut self.v_segments);
854            for (horiz, segments) in v_segments {
855                normalize_collinear_segments(segments, |top, bottom, cost| {
856                    self.add_vert_segment(horiz, top, bottom, cost);
857                });
858            }
859        }
860        {
861            let _s = tracing::info_span!("intersections").entered();
862            self.nodes = collect_intersections(self.iter_hsegs(), self.iter_vsegs());
863        }
864        // Re-segment, but now add segments for each node.
865        {
866            let _s = tracing::info_span!("resegment", nodes = self.nodes.len()).entered();
867            let mut h_segments = std::mem::take(&mut self.h_segments);
868            self.nodes.iter().for_each(|&node| {
869                h_segments
870                    .entry(node.y)
871                    .or_default()
872                    .push(hseg(node.x, node.x, COST_ZERO));
873            });
874            for (vert, segments) in h_segments {
875                normalize_collinear_segments(segments, |left, right, cost| {
876                    self.add_horiz_segment(vert, left, right, cost);
877                });
878            }
879            let mut v_segments = std::mem::take(&mut self.v_segments);
880            self.nodes.iter().for_each(|&node| {
881                v_segments
882                    .entry(node.x)
883                    .or_default()
884                    .push(vseg(node.y, node.y, COST_ZERO));
885            });
886            for (horiz, segments) in v_segments {
887                normalize_collinear_segments(segments, |top, bottom, cost| {
888                    self.add_vert_segment(horiz, top, bottom, cost);
889                });
890            }
891        }
892        // The final node set is exactly the endpoints of the now-split segments.
893        // Pass 2 injected a zero-length marker at every crossing (found by the first
894        // `collect_intersections`) and re-normalized, which splits each segment at
895        // those crossings — so every crossing is now a segment endpoint. A second
896        // intersection sweep would only recompute those same points, so skip it.
897        {
898            let _s = tracing::info_span!("endpoints").entered();
899            self.nodes = self
900                .iter_hsegs()
901                .flat_map(|(y, h_seg)| [point(h_seg.start, y), point(h_seg.end, y)])
902                .chain(
903                    self.iter_vsegs()
904                        .flat_map(|(x, v_seg)| [point(x, v_seg.start), point(x, v_seg.end)]),
905                )
906                .collect();
907        }
908        self.rebuild_graph();
909        self.dirty = false;
910    }
911    fn iter_hsegs(&self) -> impl Iterator<Item = (CoordY, HSegment)> + '_ {
912        self.h_segments
913            .iter()
914            .flat_map(|(&y, h_segs)| h_segs.iter().map(move |h_seg| (y, *h_seg)))
915    }
916    fn iter_vsegs(&self) -> impl Iterator<Item = (CoordX, VSegment)> + '_ {
917        self.v_segments
918            .iter()
919            .flat_map(|(&x, v_segs)| v_segs.iter().map(move |v_seg| (x, *v_seg)))
920    }
921    fn rebuild_graph(&mut self) {
922        let _s = tracing::info_span!("rebuild_graph", nodes = self.nodes.len()).entered();
923        let mut node_to_index: FxHashMap<Point, petgraph::graph::NodeIndex> = FxHashMap::default();
924        let edges: usize = self.h_segments.values().map(Vec::len).sum::<usize>()
925            + self.v_segments.values().map(Vec::len).sum::<usize>();
926        let mut graph = UnGraph::default();
927        {
928            let _s = tracing::info_span!("add_nodes").entered();
929            for &node in &self.nodes {
930                let index = graph.add_node(node);
931                node_to_index.insert(node, index);
932            }
933        }
934        let _e = tracing::info_span!("add_edges", edges).entered();
935        for hseg in self.iter_hsegs() {
936            let y = hseg.0;
937            let h_seg = hseg.1;
938            let start_node = point(h_seg.start, y);
939            let end_node = point(h_seg.end, y);
940            graph.add_edge(
941                node_to_index[&start_node],
942                node_to_index[&end_node],
943                h_seg.cost,
944            );
945        }
946        for vseg in self.iter_vsegs() {
947            let x = vseg.0;
948            let v_seg = vseg.1;
949            let start_node = point(x, v_seg.start);
950            let end_node = point(x, v_seg.end);
951            graph.add_edge(
952                node_to_index[&start_node],
953                node_to_index[&end_node],
954                v_seg.cost,
955            );
956        }
957        self.graph = graph;
958        self.node_to_index = node_to_index;
959    }
960    /// The grid point a node index stands for. Every index the graph hands out
961    /// resolves; a stray one yields the origin rather than taking the editor down.
962    fn point(&self, node: NodeIndex) -> Point {
963        self.graph.node_weight(node).copied().unwrap_or(Point::ZERO)
964    }
965
966    fn successors(&self, state: SearchState) -> Vec<(SearchState, Cost)> {
967        let prev_dir = state.dir;
968        let prev_point = self.point(state.node);
969        let mut north_cost: Option<Cost> = None;
970        let mut south_cost: Option<Cost> = None;
971        let mut east_cost: Option<Cost> = None;
972        let mut west_cost: Option<Cost> = None;
973        // Get the costs to move in the 4 cardinal directions from the current node.
974        for edge in self.graph.edges(state.node) {
975            let neighbor = edge.target();
976            let cost = *edge.weight();
977            let neighbor_point = self.point(neighbor);
978            if neighbor_point.x > prev_point.x {
979                east_cost = Some(cost);
980            } else if neighbor_point.x < prev_point.x {
981                west_cost = Some(cost);
982            } else if neighbor_point.y > prev_point.y {
983                south_cost = Some(cost);
984            } else {
985                north_cost = Some(cost);
986            }
987        }
988        // Calculate the east and west cost as a single cost,
989        // since if we are north/south bound, we should consider
990        // this a crossing.
991        let east_west_crossing_cost = match (east_cost, west_cost) {
992            (Some(east), Some(west)) => east.max(west),
993            _ => COST_ZERO,
994        };
995        let north_south_crossing_cost = match (north_cost, south_cost) {
996            (Some(north), Some(south)) => north.max(south),
997            _ => COST_ZERO,
998        };
999        // Rescan the edges to generate the successors with the correct costs.
1000        self.graph
1001            .edges(state.node)
1002            .map(|edge| {
1003                let neighbor = edge.target();
1004                let cost = *edge.weight();
1005                let neighbor_point = self.point(neighbor);
1006                let dir = if neighbor_point.x > prev_point.x {
1007                    Direction::East
1008                } else if neighbor_point.x < prev_point.x {
1009                    Direction::West
1010                } else if neighbor_point.y > prev_point.y {
1011                    Direction::South
1012                } else {
1013                    Direction::North
1014                };
1015                let step_length = neighbor_point.manhattan_distance(prev_point) as f64;
1016                let step_cost = turn_cost(prev_dir, dir)
1017                    + MOVE_COST * step_length
1018                    + cost * step_length
1019                    + cross_cost(
1020                        prev_dir,
1021                        dir,
1022                        east_west_crossing_cost,
1023                        north_south_crossing_cost,
1024                    );
1025                (
1026                    SearchState {
1027                        node: neighbor,
1028                        dir: Some(dir),
1029                    },
1030                    step_cost,
1031                )
1032            })
1033            .collect()
1034    }
1035    /// Route from `start` to `end`, seeding the search with `incoming` as the
1036    /// direction already being travelled (so reversing it out of `start` pays the
1037    /// `turn_cost` reversal penalty). Returns the path together with the direction
1038    /// of its final segment — the direction of travel arriving at `end` — so a
1039    /// caller routing several segments in series can forbid the next one doubling
1040    /// back.
1041    fn path_find(
1042        &mut self,
1043        start: impl Into<Point>,
1044        end: impl Into<Point>,
1045        incoming: Option<Direction>,
1046    ) -> Option<(Vec<Point>, Option<Direction>)> {
1047        self.update();
1048        let start: Point = start.into();
1049        let end: Point = end.into();
1050        let &start_node = self.node_to_index.get(&start)?;
1051        let end_node = self.node_to_index.get(&end)?;
1052        let start = SearchState {
1053            node: start_node,
1054            dir: incoming,
1055        };
1056        let result = dijkstra(
1057            &start,
1058            |state| self.successors(*state),
1059            |state| state.node == *end_node,
1060        );
1061        result.map(|(path, _cost)| {
1062            let outgoing = path.last().and_then(|state| state.dir);
1063            let points = path
1064                .into_iter()
1065                .map(|state| self.point(state.node))
1066                .collect();
1067            (points, outgoing)
1068        })
1069    }
1070    /// Route from `start` to `end`, or — when the lattice holds no path — an L
1071    /// that ignores it, marked [`Resolution::Fallback`] so the caller can tell.
1072    pub fn path_find_with_fallback(
1073        &mut self,
1074        start: impl Into<Point>,
1075        end: impl Into<Point>,
1076        incoming: Option<Direction>,
1077    ) -> Leg {
1078        let start: Point = start.into();
1079        let end: Point = end.into();
1080        if let Some((path, outgoing)) = self.path_find(start, end, incoming) {
1081            return Leg {
1082                path,
1083                outgoing,
1084                resolution: Resolution::Routed,
1085            };
1086        }
1087        let path = vec![start, point(end.x, start.y), end];
1088        let outgoing = path
1089            .windows(2)
1090            .rev()
1091            .find_map(|w| direction_between(w[0], w[1]))
1092            .or(incoming);
1093        Leg {
1094            path,
1095            outgoing,
1096            resolution: Resolution::Fallback,
1097        }
1098    }
1099}
1100
1101/// Every crossing of a horizontal segment by a vertical one, by a line sweep in
1102/// `x`: each horizontal segment contributes an Enter and an Exit event at its
1103/// ends, each vertical segment a Scan event at its `x`, and a Scan reports the
1104/// horizontal segments active at that moment whose `y` falls inside its span.
1105fn collect_intersections(
1106    h_segments: impl IntoIterator<Item = (CoordY, HSegment)>,
1107    v_segments: impl IntoIterator<Item = (CoordX, VSegment)>,
1108) -> BTreeSet<Point> {
1109    let mut events: Vec<Event<CoordX, (CoordY, CoordY)>> = h_segments
1110        .into_iter()
1111        .flat_map(|(y, h_seg)| {
1112            [
1113                Event::enter(h_seg.start, (y, y)),
1114                Event::exit(h_seg.end, (y, y)),
1115            ]
1116        })
1117        .chain(
1118            v_segments
1119                .into_iter()
1120                .map(|(x, v_seg)| Event::scan(x, (v_seg.start, v_seg.end))),
1121        )
1122        .collect::<Vec<_>>();
1123    events.sort();
1124    #[cfg(any(test, feature = "test-support"))]
1125    {
1126        use std::sync::atomic::Ordering::Relaxed;
1127        ci_stats::CALLS.fetch_add(1, Relaxed);
1128        ci_stats::EVENTS.fetch_add(events.len() as u64, Relaxed);
1129    }
1130    let mut intersections = BTreeSet::new();
1131    // Use a map to count active segments at each y-coordinate
1132    // This handles segments that touch at boundaries (e.g., one ends at x=10, another starts at x=10)
1133    let mut active_h_segments: BTreeMap<CoordY, usize> = BTreeMap::new();
1134    for event in events {
1135        match event.sense() {
1136            EventSense::Enter => {
1137                let y = event.cost().0;
1138                *active_h_segments.entry(y).or_insert(0) += 1;
1139            }
1140            EventSense::Exit => {
1141                let y = event.cost().0;
1142                if let Some(count) = active_h_segments.get_mut(&y) {
1143                    *count = count.saturating_sub(1);
1144                    if *count == 0 {
1145                        active_h_segments.remove(&y);
1146                    }
1147                }
1148            }
1149            EventSense::Scan => {
1150                let (start, end) = event.cost();
1151                // Only the active horizontal segments whose `y` falls in this
1152                // vertical segment's span intersect it. `active_h_segments` is
1153                // keyed by `y` and only holds entries with a non-zero count, so a
1154                // range query yields exactly those — no scanning past the span.
1155                let in_span = active_h_segments.range(start..=end);
1156                #[cfg(any(test, feature = "test-support"))]
1157                {
1158                    use std::sync::atomic::Ordering::Relaxed;
1159                    ci_stats::SCANS.fetch_add(1, Relaxed);
1160                    ci_stats::INNER_ITERS.fetch_add(in_span.clone().count() as u64, Relaxed);
1161                }
1162                for (&y, _) in in_span {
1163                    intersections.insert(point(event.t(), y));
1164                    #[cfg(any(test, feature = "test-support"))]
1165                    ci_stats::INTERSECTIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1166                }
1167            }
1168        }
1169    }
1170    intersections
1171}
1172
1173fn normalize_collinear_segments<T: Ord + Copy>(
1174    segments: impl IntoIterator<Item = Segment<T>>,
1175    mut maker: impl FnMut(T, T, Cost),
1176) {
1177    let mut events = segments
1178        .into_iter()
1179        .flat_map(|seg| {
1180            [
1181                Event::enter(seg.start, seg.cost),
1182                Event::exit(seg.end, seg.cost),
1183            ]
1184        })
1185        .collect::<Vec<_>>();
1186    // Sort the events by their coordinate, with Enter events before Exit events in case of ties.
1187    events.sort();
1188    scan_disjoint_segments(events, |start, end, cost| {
1189        maker(start, end, cost);
1190    });
1191}
1192
1193fn scan_disjoint_segments<T: Ord + Copy>(
1194    events: impl IntoIterator<Item = Event<T, Cost>>,
1195    mut maker: impl FnMut(T, T, Cost),
1196) {
1197    let mut events_iter = events.into_iter();
1198
1199    // Handle the first event to initialize state
1200    let Some(first_event) = events_iter.next() else {
1201        return;
1202    };
1203
1204    let mut last_t = first_event.t();
1205    let mut line_count = first_event.count();
1206    let mut current_cost = if first_event.is_enter() {
1207        first_event.cost()
1208    } else {
1209        COST_ZERO - first_event.cost()
1210    };
1211
1212    // Process remaining events
1213    for event in events_iter {
1214        let t = event.t();
1215        // Invariant: line_count > 0 means last_t was assigned in a previous iteration
1216        if line_count != 0 {
1217            maker(last_t, t, current_cost);
1218        }
1219        last_t = t;
1220        line_count += event.count();
1221        current_cost = if event.is_enter() {
1222            current_cost + event.cost()
1223        } else {
1224            current_cost - event.cost()
1225        };
1226    }
1227}
1228
1229fn interval_overlap<T: Ord + Copy>(a_start: T, a_end: T, b_start: T, b_end: T) -> bool {
1230    a_start < b_end && b_start < a_end
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235
1236    use super::*;
1237
1238    #[test]
1239    fn block_axis_index_selects_the_same_blocks_as_a_scan() {
1240        use crate::block::Block;
1241        use crate::point::point;
1242        use std::collections::BTreeSet;
1243        // Distinct x's and distinct y's so each block is identifiable per axis.
1244        let blocks = vec![
1245            Block {
1246                top_left: point(0, 0),
1247                bottom_right: point(8, 8),
1248            },
1249            Block {
1250                top_left: point(20, 4),
1251                bottom_right: point(30, 12),
1252            },
1253            Block {
1254                top_left: point(-10, -3),
1255                bottom_right: point(-2, 40),
1256            },
1257        ];
1258        let index = BlockAxisIndex::build(&blocks);
1259        for c in -14..=44 {
1260            let y = CoordY::from(c);
1261            let from_index: BTreeSet<i32> = index
1262                .spanning_y(y, &blocks)
1263                .map(|b| b.top_left.x.raw())
1264                .collect();
1265            let brute: BTreeSet<i32> = blocks
1266                .iter()
1267                .filter(|b| b.expand_y(1).spans_y(y))
1268                .map(|b| b.top_left.x.raw())
1269                .collect();
1270            assert_eq!(from_index, brute, "spanning_y mismatch at y={c}");
1271
1272            let x = CoordX::from(c);
1273            let from_index_x: BTreeSet<i32> = index
1274                .spanning_x(x, &blocks)
1275                .map(|b| b.top_left.y.raw())
1276                .collect();
1277            let brute_x: BTreeSet<i32> = blocks
1278                .iter()
1279                .filter(|b| b.expand_x(1).spans_x(x))
1280                .map(|b| b.top_left.y.raw())
1281                .collect();
1282            assert_eq!(from_index_x, brute_x, "spanning_x mismatch at x={c}");
1283        }
1284    }
1285
1286    macro_rules! hseg {
1287        (y=$y:expr, [$(($start:expr => $end:expr, $cost:expr)),* $(,)?]) => {
1288            BTreeMap::from([(
1289                CoordY::from($y),
1290                vec![
1291                    $(HSegment {
1292                        start: CoordX::from($start),
1293                        end: CoordX::from($end),
1294                        cost: $cost.into(),
1295                    }),*
1296                ]
1297            )])
1298        };
1299    }
1300
1301    macro_rules! vseg {
1302        (x=$x:expr, [$(($start:expr => $end:expr, $cost:expr)),* $(,)?]) => {
1303            BTreeMap::from([(
1304                CoordX::from($x),
1305                vec![
1306                    $(VSegment {
1307                        start: CoordY::from($start),
1308                        end: CoordY::from($end),
1309                        cost: $cost.into(),
1310                    }),*
1311                ]
1312            )])
1313        };
1314    }
1315
1316    // Brute force algorithm
1317    fn collect_intersections_brute_force(
1318        h_segments: impl IntoIterator<Item = (CoordY, HSegment)>,
1319        v_segments: impl IntoIterator<Item = (CoordX, VSegment)>,
1320    ) -> Vec<Point> {
1321        let mut points = vec![];
1322        let v_segments = v_segments.into_iter().collect::<Vec<_>>();
1323        for (y, hseg) in h_segments {
1324            for (x, vseg) in &v_segments {
1325                if hseg.start <= *x && hseg.end >= *x && vseg.start <= y && vseg.end >= y {
1326                    points.push(point(*x, y));
1327                }
1328            }
1329        }
1330        points
1331    }
1332
1333    #[test]
1334    fn test_vseed() {
1335        let mut router = RouterNGBuilder::default().build();
1336        router.seed_vert_channel(point(0, 0), 1.0);
1337        router.update();
1338        assert_eq!(
1339            router.v_segments,
1340            BTreeMap::from([(CoordX::from(0), vec![vseg(NEG_INFINITY_Y, INFINITY_Y, 1.0)])])
1341        );
1342    }
1343
1344    #[test]
1345    fn test_normalize() {
1346        let mut router = RouterNGBuilder::default().build();
1347        router.add_horiz_segment(0, 0, 10, 1.0);
1348        router.add_horiz_segment(0, 5, 15, 2.0);
1349        router.update();
1350        assert_eq!(
1351            router.h_segments,
1352            hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)])
1353        );
1354    }
1355
1356    #[test]
1357    fn test_normalize_complete_overlap() {
1358        // One segment completely contains another
1359        let mut router = RouterNGBuilder::default().build();
1360        router.add_horiz_segment(0, 0, 20, 1.0);
1361        router.add_horiz_segment(0, 5, 15, 2.0);
1362        router.update();
1363        assert_eq!(
1364            router.h_segments,
1365            hseg!(y=0, [(0=>5, 1.0), (5=>15, 3.0), (15=>20, 1.0)])
1366        );
1367    }
1368
1369    #[test]
1370    fn test_normalize_no_overlap() {
1371        // Segments don't overlap at all
1372        let mut router = RouterNGBuilder::default().build();
1373        router.add_horiz_segment(0, 0, 10, 1.0);
1374        router.add_horiz_segment(0, 20, 30, 2.0);
1375        router.update();
1376        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 1.0), (20=>30, 2.0)]));
1377    }
1378
1379    #[test]
1380    fn test_normalize_adjacent_segments() {
1381        // Segments touch at endpoints but don't overlap
1382        let mut router = RouterNGBuilder::default().build();
1383        router.add_horiz_segment(0, 0, 10, 1.0);
1384        router.add_horiz_segment(0, 10, 20, 2.0);
1385        router.update();
1386        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 1.0), (10=>20, 2.0)]));
1387    }
1388
1389    #[test]
1390    fn test_normalize_triple_overlap() {
1391        // Three segments with various overlaps
1392        let mut router = RouterNGBuilder::default().build();
1393        router.add_horiz_segment(0, 0, 15, 1.0);
1394        router.add_horiz_segment(0, 5, 20, 2.0);
1395        router.add_horiz_segment(0, 10, 25, 3.0);
1396        router.update();
1397        assert_eq!(
1398            router.h_segments,
1399            hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 6.0), (15=>20, 5.0), (20=>25, 3.0)])
1400        );
1401    }
1402
1403    #[test]
1404    fn test_normalize_multiple_rows() {
1405        // Segments on different rows should be handled independently
1406        let mut router = RouterNGBuilder::default().build();
1407        router.add_horiz_segment(0, 0, 10, 1.0);
1408        router.add_horiz_segment(0, 5, 15, 2.0);
1409        router.add_horiz_segment(5, 0, 10, 3.0);
1410        router.add_horiz_segment(5, 5, 15, 4.0);
1411        router.update();
1412
1413        let mut expected = BTreeMap::new();
1414        expected.extend(hseg!(y=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)]));
1415        expected.extend(hseg!(y=5, [(0=>5, 3.0), (5=>10, 7.0), (10=>15, 4.0)]));
1416        assert_eq!(router.h_segments, expected);
1417    }
1418
1419    #[test]
1420    fn test_normalize_negative_coords() {
1421        // Segments in negative coordinate space
1422        let mut router = RouterNGBuilder::default().build();
1423        router.add_horiz_segment(-5, -20, -10, 1.0);
1424        router.add_horiz_segment(-5, -15, -5, 2.0);
1425        router.update();
1426        assert_eq!(
1427            router.h_segments,
1428            hseg!(y=-5, [(-20 => -15, 1.0), (-15 => -10, 3.0), (-10 => -5, 2.0)])
1429        );
1430    }
1431
1432    #[test]
1433    fn test_normalize_vertical_segments() {
1434        // Test vertical segment normalization
1435        let mut router = RouterNGBuilder::default().build();
1436        router.add_vert_segment(0, 0, 10, 1.0);
1437        router.add_vert_segment(0, 5, 15, 2.0);
1438        router.update();
1439        assert_eq!(
1440            router.v_segments,
1441            vseg!(x=0, [(0=>5, 1.0), (5=>10, 3.0), (10=>15, 2.0)])
1442        );
1443    }
1444
1445    #[test]
1446    fn test_normalize_identical_segments() {
1447        // Same segment added twice
1448        let mut router = RouterNGBuilder::default().build();
1449        router.add_horiz_segment(0, 0, 10, 1.0);
1450        router.add_horiz_segment(0, 0, 10, 1.0);
1451        router.update();
1452        assert_eq!(router.h_segments, hseg!(y=0, [(0=>10, 2.0)]));
1453    }
1454
1455    #[test]
1456    fn test_normalize_reverse_order() {
1457        // Segments added in decreasing coordinate order
1458        let mut router = RouterNGBuilder::default().build();
1459        router.add_horiz_segment(0, 20, 30, 1.0);
1460        router.add_horiz_segment(0, 10, 25, 2.0);
1461        router.add_horiz_segment(0, 0, 15, 3.0);
1462        router.update();
1463        assert_eq!(
1464            router.h_segments,
1465            hseg!(y=0, [(0=>10, 3.0), (10=>15, 5.0), (15=>20, 2.0), (20=>25, 3.0), (25=>30, 1.0)])
1466        );
1467    }
1468
1469    // Tests for collect_intersections function
1470
1471    #[test]
1472    fn test_collect_intersections_no_intersections() {
1473        // Horizontal segments with no vertical intersections
1474        let h_segs = vec![
1475            (CoordY::from(0), hseg(0, 10, 1.0)),
1476            (CoordY::from(5), hseg(15, 25, 1.0)),
1477        ];
1478        // Vertical segment outside the y-range of all horizontal segments
1479        let v_segs = vec![(CoordX::from(20), vseg(10, 15, 1.0))];
1480        let intersections = collect_intersections(h_segs, v_segs);
1481        assert_eq!(intersections, BTreeSet::new());
1482    }
1483
1484    #[test]
1485    fn test_collect_intersections_at_boundaries() {
1486        // Intersections at segment boundaries (start/end)
1487        let h_segs = vec![(CoordY::from(5), hseg(0, 10, 1.0))];
1488        // Vertical segment at the start of horizontal segment
1489        let v_segs_start = vec![(CoordX::from(0), vseg(0, 10, 1.0))];
1490        let intersections = collect_intersections(h_segs.clone(), v_segs_start);
1491        assert_eq!(intersections, BTreeSet::from([point(0, 5)]));
1492
1493        // Vertical segment at the end of horizontal segment
1494        let v_segs_end = vec![(CoordX::from(10), vseg(0, 10, 1.0))];
1495        let intersections = collect_intersections(h_segs.clone(), v_segs_end);
1496        assert_eq!(intersections, BTreeSet::from([point(10, 5)]));
1497
1498        // Vertical segment spanning from horizontal's y-coordinate exactly
1499        let v_segs_y_start = vec![(CoordX::from(5), vseg(5, 15, 1.0))];
1500        let intersections = collect_intersections(h_segs.clone(), v_segs_y_start);
1501        assert_eq!(intersections, BTreeSet::from([point(5, 5)]));
1502
1503        // Vertical segment ending at horizontal's y-coordinate exactly
1504        let v_segs_y_end = vec![(CoordX::from(5), vseg(0, 5, 1.0))];
1505        let intersections = collect_intersections(h_segs, v_segs_y_end);
1506        assert_eq!(intersections, BTreeSet::from([point(5, 5)]));
1507    }
1508
1509    #[test]
1510    fn test_collect_intersections_corners() {
1511        // Corner case: vertical start == horizontal start
1512        let h_segs = vec![(CoordY::from(5), hseg(10, 20, 1.0))];
1513        let v_segs = vec![(CoordX::from(10), vseg(5, 15, 1.0))];
1514        let intersections = collect_intersections(h_segs, v_segs);
1515        assert_eq!(intersections, BTreeSet::from([point(10, 5)]));
1516    }
1517
1518    #[test]
1519    fn test_collect_intersections_corner_all_endpoints() {
1520        // All four corners: (h.start, v.start), (h.start, v.end), (h.end, v.start), (h.end, v.end)
1521        let h_seg_y = CoordY::from(10);
1522        let h_segs = vec![(h_seg_y, hseg(5, 15, 1.0))];
1523
1524        // Test (h.start, v.start) corner
1525        let v_segs = vec![(CoordX::from(5), vseg(10, 20, 1.0))];
1526        let intersections = collect_intersections(h_segs.clone(), v_segs);
1527        assert_eq!(intersections, BTreeSet::from([point(5, 10)]));
1528
1529        // Test (h.start, v.end) corner
1530        let v_segs = vec![(CoordX::from(5), vseg(0, 10, 1.0))];
1531        let intersections = collect_intersections(h_segs.clone(), v_segs);
1532        assert_eq!(intersections, BTreeSet::from([point(5, 10)]));
1533
1534        // Test (h.end, v.start) corner
1535        let v_segs = vec![(CoordX::from(15), vseg(10, 20, 1.0))];
1536        let intersections = collect_intersections(h_segs.clone(), v_segs);
1537        assert_eq!(intersections, BTreeSet::from([point(15, 10)]));
1538
1539        // Test (h.end, v.end) corner
1540        let v_segs = vec![(CoordX::from(15), vseg(0, 10, 1.0))];
1541        let intersections = collect_intersections(h_segs, v_segs);
1542        assert_eq!(intersections, BTreeSet::from([point(15, 10)]));
1543    }
1544
1545    #[test]
1546    fn test_collect_intersections_multiple() {
1547        // Multiple intersections from a single vertical segment crossing multiple horizontal segments
1548        let h_segs = vec![
1549            (CoordY::from(5), hseg(0, 20, 1.0)),
1550            (CoordY::from(10), hseg(0, 20, 1.0)),
1551            (CoordY::from(15), hseg(0, 20, 1.0)),
1552        ];
1553        let v_segs = vec![(CoordX::from(10), vseg(0, 20, 1.0))];
1554        let intersections = collect_intersections(h_segs, v_segs);
1555        assert_eq!(
1556            intersections,
1557            BTreeSet::from([point(10, 5), point(10, 10), point(10, 15)])
1558        );
1559    }
1560
1561    #[test]
1562    fn test_collect_intersections_multiple_verticals() {
1563        // Single horizontal segment crossing multiple vertical segments
1564        let h_segs = vec![(CoordY::from(10), hseg(0, 30, 1.0))];
1565        let v_segs = vec![
1566            (CoordX::from(5), vseg(5, 15, 1.0)),
1567            (CoordX::from(15), vseg(5, 15, 1.0)),
1568            (CoordX::from(25), vseg(5, 15, 1.0)),
1569        ];
1570        let intersections = collect_intersections(h_segs, v_segs);
1571        assert_eq!(
1572            intersections,
1573            BTreeSet::from([point(5, 10), point(15, 10), point(25, 10)])
1574        );
1575    }
1576
1577    #[test]
1578    fn test_collect_intersections_vertical_outside_horizontal_y_range() {
1579        // Vertical segment exists in x-range of horizontal but y is outside
1580        let h_segs = vec![(CoordY::from(10), hseg(0, 20, 1.0))];
1581        let v_segs = vec![(CoordX::from(10), vseg(15, 25, 1.0))];
1582        let intersections = collect_intersections(h_segs, v_segs);
1583        assert_eq!(intersections, BTreeSet::new());
1584    }
1585
1586    #[test]
1587    fn test_collect_intersections_empty_inputs() {
1588        // Empty horizontal segments
1589        let intersections =
1590            collect_intersections(vec![], vec![(CoordX::from(5), vseg(0, 10, 1.0))]);
1591        assert_eq!(intersections, BTreeSet::new());
1592
1593        // Empty vertical segments
1594        let intersections =
1595            collect_intersections(vec![(CoordY::from(5), hseg(0, 10, 1.0))], vec![]);
1596        assert_eq!(intersections, BTreeSet::new());
1597
1598        // Both empty
1599        let intersections: BTreeSet<Point> = collect_intersections(
1600            Vec::<(CoordY, HSegment)>::new(),
1601            Vec::<(CoordX, VSegment)>::new(),
1602        );
1603        assert_eq!(intersections, BTreeSet::new());
1604    }
1605
1606    #[test]
1607    fn test_random_segments_line_sweep_matches_brute_force() {
1608        use rand::rngs::StdRng;
1609        use rand::{Rng, SeedableRng};
1610
1611        const NUM_H_SEGMENTS: usize = 1000;
1612        const NUM_V_SEGMENTS: usize = 1000;
1613        const FIELD_SIZE: i32 = 200;
1614
1615        // Use a fixed seed for reproducibility
1616        let mut rng = StdRng::seed_from_u64(42);
1617
1618        let mut router = RouterNGBuilder::default().build();
1619
1620        // Generate random horizontal segments
1621        for _ in 0..NUM_H_SEGMENTS {
1622            let y = rng.random_range(0..FIELD_SIZE);
1623            let x1 = rng.random_range(0..FIELD_SIZE);
1624            let x2 = rng.random_range(0..FIELD_SIZE);
1625            let (start, end) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
1626            // Ensure non-zero length segments
1627            if start < end {
1628                router.add_horiz_segment(y, start, end, rng.random_range(0.1..10.0));
1629            }
1630        }
1631
1632        // Generate random vertical segments
1633        for _ in 0..NUM_V_SEGMENTS {
1634            let x = rng.random_range(0..FIELD_SIZE);
1635            let y1 = rng.random_range(0..FIELD_SIZE);
1636            let y2 = rng.random_range(0..FIELD_SIZE);
1637            let (start, end) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
1638            // Ensure non-zero length segments
1639            if start < end {
1640                router.add_vert_segment(x, start, end, rng.random_range(0.1..10.0));
1641            }
1642        }
1643
1644        // Normalize segments (merge overlapping segments)
1645        let normalize_start = std::time::Instant::now();
1646        router.update();
1647        let normalize_time = normalize_start.elapsed();
1648
1649        println!("Normalization took: {normalize_time:?}");
1650        println!(
1651            "Normalized to {} horizontal segments and {} vertical segments",
1652            router
1653                .h_segments
1654                .values()
1655                .map(std::vec::Vec::len)
1656                .sum::<usize>(),
1657            router
1658                .v_segments
1659                .values()
1660                .map(std::vec::Vec::len)
1661                .sum::<usize>()
1662        );
1663
1664        // Collect intersections using line-sweep algorithm
1665        let line_sweep_start = std::time::Instant::now();
1666        let line_sweep_intersections =
1667            collect_intersections(router.iter_hsegs(), router.iter_vsegs());
1668        let line_sweep_time = line_sweep_start.elapsed();
1669
1670        println!("Line-sweep algorithm took: {line_sweep_time:?}");
1671        println!("Found {} intersections", line_sweep_intersections.len());
1672
1673        // Collect intersections using brute force algorithm
1674        let brute_force_start = std::time::Instant::now();
1675        let brute_force_intersections: BTreeSet<Point> =
1676            collect_intersections_brute_force(router.iter_hsegs(), router.iter_vsegs())
1677                .into_iter()
1678                .collect();
1679        let brute_force_time = brute_force_start.elapsed();
1680
1681        println!("Brute-force algorithm took: {brute_force_time:?}");
1682
1683        let speedup = brute_force_time.as_secs_f64() / line_sweep_time.as_secs_f64();
1684        println!("Line-sweep is {speedup:.2}x faster than brute-force");
1685
1686        // Compare results
1687        assert_eq!(
1688            line_sweep_intersections.len(),
1689            brute_force_intersections.len(),
1690            "Number of intersections differs: line-sweep found {}, brute-force found {}",
1691            line_sweep_intersections.len(),
1692            brute_force_intersections.len()
1693        );
1694
1695        assert_eq!(
1696            line_sweep_intersections, brute_force_intersections,
1697            "Intersection sets differ between line-sweep and brute-force algorithms"
1698        );
1699    }
1700
1701    #[test]
1702    fn incoming_direction_prevents_double_back_out_of_waypoint() {
1703        // Four free channels (no blocks) form a 2x2 grid with corners at
1704        // (0,0), (10,0), (0,-10), (10,-10). The target sits north-east of the
1705        // start, reachable equally cheaply by "east then north" or "north then
1706        // east" — the tie is broken only by the incoming direction.
1707        let mut router = RouterNGBuilder::default().build();
1708        router.seed_channels(point(0, 0), 0.0);
1709        router.seed_channels(point(10, -10), 0.0);
1710
1711        // Arriving at the start heading south, the route must turn east first:
1712        // leaving north would reverse back over the incoming wire (the kink) and
1713        // is forbidden by the reversal penalty.
1714        let Leg {
1715            path,
1716            outgoing,
1717            resolution,
1718        } = router.path_find_with_fallback(point(0, 0), point(10, -10), Some(Direction::South));
1719        assert_eq!(resolution, Resolution::Routed, "the lattice holds a path");
1720
1721        assert!(path.len() >= 2, "expected a real path, got {path:?}");
1722        assert!(
1723            path[1].x > path[0].x,
1724            "router doubled back instead of turning east first: {path:?}"
1725        );
1726        // Its final segment climbs north into the target — the direction threaded
1727        // to the next segment.
1728        assert_eq!(outgoing, Some(Direction::North));
1729    }
1730}
1731
1732#[cfg(test)]
1733mod fallback {
1734    use super::*;
1735
1736    /// A leg the lattice holds no path for still comes back — as an L — but
1737    /// says so, where it used to pass for a routed one.
1738    #[test]
1739    fn a_leg_with_no_path_is_marked_as_a_fallback() {
1740        let mut router = RouterNGBuilder::default().build();
1741        router.seed_channels(point(0, 0), 0.0);
1742        let unseeded = point(10, -10);
1743        assert!(
1744            !router.node_to_index.contains_key(&unseeded),
1745            "precondition: the target is no node of the lattice"
1746        );
1747        let leg = router.path_find_with_fallback(point(0, 0), unseeded, None);
1748        assert_eq!(leg.resolution, Resolution::Fallback);
1749        assert_eq!(leg.path, vec![point(0, 0), point(10, 0), unseeded]);
1750    }
1751}
1752
1753#[cfg(test)]
1754mod foreground_replay {
1755    use super::*;
1756
1757    fn blocks() -> [(Point, Point); 3] {
1758        [
1759            (point(0, 0), point(8, 8)),
1760            (point(24, 0), point(32, 8)),
1761            (point(12, 20), point(20, 28)),
1762        ]
1763    }
1764
1765    /// Build every piece into one lattice, with `raised` added last — the order
1766    /// a whole build would use if the foreground routed last.
1767    fn whole(raised: &[(Point, Point)], seeds: &[Point]) -> ClosedRouter {
1768        let mut builder = RouterNGBuilder::default();
1769        for (tl, br) in blocks() {
1770            builder.add_block(tl, br);
1771        }
1772        for &(tl, br) in raised {
1773            builder.add_block(tl, br);
1774        }
1775        for &p in seeds {
1776            builder.add_seed_point(p);
1777        }
1778        builder.build_closed()
1779    }
1780
1781    /// Build the background without `raised`, then replay it onto a clone.
1782    fn background_plus_foreground(raised: &[(Point, Point)], seeds: &[Point]) -> ClosedRouter {
1783        let mut builder = RouterNGBuilder::default();
1784        for (tl, br) in blocks() {
1785            builder.add_block(tl, br);
1786        }
1787        let background = builder.build_closed();
1788        background.clone().extended(|opening| {
1789            for &(tl, br) in raised {
1790                opening.add_block(tl, br);
1791            }
1792            for &p in seeds {
1793                opening.add_seed_point(p);
1794            }
1795        })
1796    }
1797
1798    /// **The property the whole design rests on**: a cached background with the
1799    /// foreground replayed onto it is the same lattice as one built whole with
1800    /// the foreground last.
1801    #[test]
1802    fn a_replayed_foreground_is_the_same_lattice_as_a_whole_build() {
1803        let raised = [(point(40, 12), point(48, 20))];
1804        let seeds = [point(10, 4), point(36, 16), point(44, 30)];
1805
1806        let whole = whole(&raised, &seeds);
1807        let replayed = background_plus_foreground(&raised, &seeds);
1808
1809        let (a, b) = (whole.fingerprint(), replayed.fingerprint());
1810        assert_eq!(a, b, "{}", a.difference(&b));
1811    }
1812
1813    /// Seeds alone — no block raised — must hold too: it is the wire-only
1814    /// gesture, and the one that adds channels without clipping anything.
1815    #[test]
1816    fn replaying_seed_points_alone_is_the_same_lattice() {
1817        let seeds = [point(10, 4), point(36, 16)];
1818        let whole = whole(&[], &seeds);
1819        let replayed = background_plus_foreground(&[], &seeds);
1820
1821        let (a, b) = (whole.fingerprint(), replayed.fingerprint());
1822        assert_eq!(a, b, "{}", a.difference(&b));
1823    }
1824
1825    /// And the lattices route the same, which is the claim the fingerprint
1826    /// stands in for: dijkstra can break ties by node index, so equal structure
1827    /// is worth checking against equal outcomes at least once.
1828    #[test]
1829    fn a_replayed_foreground_routes_the_same_as_a_whole_build() {
1830        let raised = [(point(40, 12), point(48, 20))];
1831        let seeds = [point(10, 4), point(36, 16), point(44, 30)];
1832        let mut whole = whole(&raised, &seeds);
1833        let mut replayed = background_plus_foreground(&raised, &seeds);
1834
1835        for (from, to) in [
1836            (point(10, 4), point(44, 30)),
1837            (point(36, 16), point(10, 4)),
1838            (point(44, 30), point(36, 16)),
1839        ] {
1840            let a = whole.route_leg(from, to, None).path;
1841            let b = replayed.route_leg(from, to, None).path;
1842            assert!(!a.is_empty(), "{from:?}→{to:?} found no path to compare");
1843            assert_eq!(a, b, "{from:?}→{to:?} routed differently");
1844        }
1845    }
1846}
1847
1848#[cfg(test)]
1849mod bounded {
1850    use super::*;
1851
1852    /// A bounded lattice reaches nowhere outside its bounds: channels stop at
1853    /// the edge, a block beyond it is not an obstacle it needs, and a seed
1854    /// point outside seeds nothing.
1855    #[test]
1856    fn a_bounded_lattice_holds_nothing_outside_its_bounds() {
1857        let bounds = Bounds::between(point(0, 0), point(40, 40));
1858        let mut builder = RouterNGBuilder::default().within(bounds);
1859        builder.add_block(point(8, 8), point(16, 16));
1860        builder.add_block(point(200, 200), point(208, 208));
1861        builder.add_seed_point(point(4, 4));
1862        builder.add_seed_point(point(300, 300));
1863        let router = builder.build_closed();
1864
1865        let nodes = router.fingerprint();
1866        assert!(!nodes.points().is_empty(), "the bounded lattice is empty");
1867        for &node in nodes.points() {
1868            assert!(
1869                bounds.holds(node),
1870                "{node:?} escaped the bounds it was built within"
1871            );
1872        }
1873    }
1874
1875    /// And it still holds what is inside: the same region built unbounded
1876    /// contains every node the bounded one does.
1877    #[test]
1878    fn a_bounded_lattice_is_what_an_unbounded_one_holds_inside_those_bounds() {
1879        let bounds = Bounds::between(point(0, 0), point(40, 40));
1880        let seed = point(4, 4);
1881        let block = (point(8, 8), point(16, 16));
1882
1883        let mut bounded = RouterNGBuilder::default().within(bounds);
1884        bounded.add_block(block.0, block.1);
1885        bounded.add_seed_point(seed);
1886        let bounded = bounded.build_closed();
1887
1888        let mut whole = RouterNGBuilder::default();
1889        whole.add_block(block.0, block.1);
1890        whole.add_seed_point(seed);
1891        let whole = whole.build_closed();
1892
1893        let inside: BTreeSet<Point> = whole
1894            .fingerprint()
1895            .points()
1896            .iter()
1897            .copied()
1898            .filter(|&p| bounds.holds(p))
1899            .collect();
1900        let held: BTreeSet<Point> = bounded.fingerprint().points().iter().copied().collect();
1901        assert!(
1902            inside.difference(&held).next().is_none(),
1903            "the bounded lattice is missing nodes the unbounded one has inside the bounds: {:?}",
1904            inside.difference(&held).take(4).collect::<Vec<_>>()
1905        );
1906    }
1907}