Skip to main content

monospace_core/shape/
connector.rs

1//! The connector: two endpoints, two terminals and a derived route between them. See _The initial
2//! set_ and _The route of a connector_ in [`docs/model.md`](../../../docs/model.md).
3//!
4//! # Design notes
5//!
6//! Nothing outside this module observes the rule below beyond the picture it produces. It decides
7//! which of several paths — every one of them satisfying _The route of a connector_ — this
8//! implementation draws.
9//!
10//! The route is the path ranked first by [`Cost`], whose fields are the four terms of the ranking
11//! in order. What the code cannot say is why each term is there.
12//!
13//! **1, the fewest bends, and 2, the shortest.** These two are the rule; the other two only break
14//! its ties. Together they replaced a bound — a route was once required to stay inside the
15//! rectangle its two starting positions span — because the shortest path is what that bound was
16//! trying to name. Dropping it cost the model a construct and gained 134 arrangements that had
17//! drawn two disconnected heads, since a rectangle one cell thick holds no alternating path at
18//! all. Term 2 decides nothing on a monotone path, where every candidate is the same length, so
19//! it is invisible across most of the characterization and easy to break without a picture
20//! moving. [`RouteRectangle`] survives only as the span term 3 measures its middle within, and
21//! bounds nothing.
22//!
23//! **3, nearest the middle.** The first two leave many candidates level, because a free run may
24//! sit anywhere, so something has to choose and the middle is the choice that reads as centred.
25//! Where a span holds an even number of cells that middle falls between two and names no winner;
26//! it is taken nearer the endpoint the connector leaves from, because a connector runs from its `from`
27//! to its `to` and that is the only thing in the arrangement that tells the two candidates apart.
28//! The cost is that the same connector described from its other end turns at the other of the two.
29//! Both pictures are right, which is what makes this a tie-break rather than a defect.
30//!
31//! **4, to the right of the connector's own travel.** Once nothing bounds a route, an arrangement
32//! whose two starting positions share a row or a column has two candidates that are exact mirror
33//! images about that line — equidistant from the middle by construction, so term 3 cannot reach
34//! them. Of the 1856 renderings in the characterization, 84 are this one shape. It is phrased as
35//! a handedness rather than as "the smaller coordinate" so that it is the same principle as term
36//! 3 instead of a second, unrelated one; which of the two mirrors counts as the right is
37//! arbitrary, was picked by looking at both rendered, and nothing distinguishes it from the
38//! other. Coordinates grow rightward and downward, so leaving `Up` or `Right` puts the larger
39//! coordinate to the right — [`is_right_of_travel`] says it in those words, because a handedness
40//! under a `y` that grows downward is what a reader gets backwards.
41//!
42//! Two endpoints at one position put both heads on one cell and only one of them can be seen. It
43//! is the `to` endpoint's, drawn over the other, for the same reason term 3 rounds toward the
44//! `from`.
45//!
46//! **Why a search rather than a construction.** [`derive_path`] was once a list of route shapes,
47//! and a list has to be argued complete. That argument failed twice, both times by omission and
48//! both times silently — a missing shape does not error, it draws the second-best picture or
49//! nothing. A ranking is a cost function, so the implementation that reads against it is the one
50//! that minimizes it, and completeness stops being an argument. [`Lattice`] is what keeps the
51//! cost fixed instead of proportional to the distance between the endpoints, which this crate
52//! needs because the core compiles to WebAssembly. Its own claim — that a route turns only on
53//! those lines — is confirmed against an unrestricted search rather than proved, and a term added
54//! to [`Cost`] that the lattice knows nothing about is exactly how that confirmation goes stale.
55//! Re-run it against any new term.
56//!
57//! ## What a terminal writes
58//!
59//! Nothing outside this module observes either rule below beyond the picture it produces. The
60//! vocabulary a terminal may have, and what each member writes in the model's own words, is
61//! _What a terminal writes_ in [`docs/model.md`](../../../docs/model.md) — this section says only
62//! what this module does with it.
63//!
64//! **Three values of one field, and not a field and its absence.** That is what the tag is for. An
65//! endpoint always carries a terminal, so a fourth one arrives as a new variant, a new tag, and one
66//! line in the model's vocabulary — the shape of a description does not change and no record
67//! reopens. A field and its absence would instead make "no terminal" a fourth spelling, and a
68//! caller who left the field out would get a connector that draws nothing at either end rather than a
69//! refusal naming the values that were expected.
70//!
71//! **A glyph and a symbol decide every side; an arm leaves three undecided.** That difference is
72//! the whole of what the three write, and it is not this module's rule to state: _Stamping_ in
73//! [`docs/model.md`](../../../docs/model.md) already decides what two figures sharing a cell
74//! leave there, and these three land in it as rows of that table. What this module owns is
75//! narrower — which side an arm goes on, in whose stroke, and which symbol a symbol terminal names.
76//! The side is the leaving direction's own, because the arm faces the route, and the stroke is the
77//! connector's own because a terminal choosing its stroke would be a second thing to keep
78//! consistent with the route it joins, while the model's arm is a stroke cell of the figure. The
79//! symbol points against the leaving direction for the reason the model gives a glyph's glyph the
80//! same rule: a terminal faces the connector it ends, so the two cannot disagree.
81//!
82//! **A symbol terminal is the head that follows the set.** Its direction is derived and only its
83//! look is the caller's, so a catalog that holds the style redraws every head that named it, and a
84//! set a build adds a style to reaches a picture nobody edited. A style the catalog does not hold
85//! draws a space, because a symbol cell's miss is final and never falls back to another style — the
86//! rule _A cell can name a symbol instead_ states, and not a choice this module makes. Only the
87//! four `Direction*` symbols are reachable from here: the other three name a shape with no
88//! direction to point from, and a terminal that wanted one would be a fourth variant rather than
89//! this one.
90//!
91//! **The set is closed at three, and is not `#[non_exhaustive]`.** A fourth terminal is a new
92//! variant in a later slice, which is a change to this enum and to the model rather than an
93//! invisible one. Marking it non-exhaustive would let a caller match on it exhaustively today and
94//! go on compiling after a fourth member exists, which is the one outcome the model's sentence
95//! about naming one more of them is written to prevent.
96
97use std::cmp::Reverse;
98use std::collections::{BinaryHeap, HashMap};
99
100use crate::cell::Side;
101use crate::shape::fragment::end::End;
102use crate::shape::fragment::literal::Literal;
103use crate::shape::fragment::symbol::NamedSymbol;
104use crate::shape::route::Route;
105use crate::{
106    Direction, Glyph, Orientation, Pos, Shape, Stroke, Surface, Symbol, SymbolCell, SymbolStyleName,
107};
108
109/// Where a connector ends: a position, the direction it leaves in, and a terminal. See _The initial
110/// set_ and _What a terminal writes_ in
111/// [`docs/model.md`](../../../docs/model.md).
112#[derive(Debug)]
113pub struct Endpoint {
114    /// The endpoint's position. The terminal occupies this position itself.
115    pub at: Pos,
116    /// The direction the connector leaves this endpoint in. The route's starting position is one
117    /// step from `at` in this direction.
118    pub leaving: Direction,
119    /// What this endpoint contributes to the cell at `at`.
120    pub terminal: Terminal,
121}
122
123impl Endpoint {
124    fn draw(&self, stroke: &Stroke, surface: &mut dyn Surface) {
125        match &self.terminal {
126            Terminal::Glyph { glyph } => Literal {
127                at: self.at,
128                glyph: glyph.clone(),
129            }
130            .draw(surface),
131            Terminal::Symbol { style } => NamedSymbol {
132                at: self.at,
133                cell: SymbolCell {
134                    symbol: symbol_for(self.leaving),
135                    style: style.clone(),
136                },
137            }
138            .draw(surface),
139            Terminal::Arm => End {
140                at: self.at,
141                side: arm_side(self.leaving),
142                stroke: stroke.clone(),
143            }
144            .draw(surface),
145        }
146    }
147}
148
149/// What an endpoint contributes to the cell at its own position. A caller builds a variant as a
150/// literal, and reads one back by matching this public field.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum Terminal {
153    /// One chosen glyph, written as a literal: decided on every side, so nothing composes into it.
154    Glyph {
155        /// The caller's glyph, spelled out rather than named. No set answers it and no style can
156        /// re-spell it, which is what a symbol terminal is for.
157        glyph: Glyph,
158    },
159    /// One named symbol, in a style the caller names: decided on every side, so nothing composes
160    /// into it. The symbol points against `leaving`, so the caller names the look and not the
161    /// direction.
162    Symbol {
163        /// The style the symbol is drawn in. The catalog answers the pair at render, and a pair it
164        /// does not hold draws a space.
165        style: SymbolStyleName,
166    },
167    /// One arm, in the connector's own stroke, on the side `leaving` names, leaving the other three
168    /// sides undecided.
169    Arm,
170}
171
172/// The side an arm terminal writes: the leaving direction's own side, and not its opposite, because
173/// the arm faces the route — which begins one step from `at` in that direction.
174///
175/// A function and not a field on [`Terminal`] because [`Side`] is crate-private. A caller can name
176/// a `Terminal::Arm` and get the side the connector leaves in; it cannot name a side of its own.
177fn arm_side(leaving: Direction) -> Side {
178    match leaving {
179        Direction::Up => Side::Top,
180        Direction::Right => Side::Right,
181        Direction::Down => Side::Bottom,
182        Direction::Left => Side::Left,
183    }
184}
185
186/// The symbol pointing in `dir`: the bridge between the four directions and the four [`Symbol`]
187/// values that point, which is a bijection.
188fn pointing(dir: Direction) -> Symbol {
189    match dir {
190        Direction::Up => Symbol::DirectionUp,
191        Direction::Right => Symbol::DirectionRight,
192        Direction::Down => Symbol::DirectionDown,
193        Direction::Left => Symbol::DirectionLeft,
194    }
195}
196
197/// The symbol a symbol terminal writes: the one pointing against `leaving`, because a terminal
198/// faces the connector it ends — the same rule _What a terminal writes_ states for a glyph
199/// terminal's glyph.
200///
201/// Only the four `Direction*` members of [`Symbol`] are reachable from here: a diamond, a hexagon
202/// and a circle point nowhere, and a terminal always has a leaving direction to point against.
203///
204/// Like [`arm_side`], a function rather than a field on [`Terminal`]: the caller names the style
205/// and cannot name a symbol disagreeing with the direction the endpoint leaves in.
206fn symbol_for(leaving: Direction) -> Symbol {
207    pointing(opposite(leaving))
208}
209
210/// A connector: two endpoints and a stroke.
211///
212/// A **complete** shape in the sense of _Complete and fragment_ in
213/// [`docs/model.md`](../../../docs/model.md). The route between the two endpoints is derived from
214/// their positions and leaving directions alone, and is never described by the caller.
215#[derive(Debug)]
216pub struct Connector {
217    /// One endpoint of the connector.
218    pub from: Endpoint,
219    /// The other endpoint of the connector.
220    pub to: Endpoint,
221    /// The stroke the route between the two endpoints is drawn in, and the stroke an arm terminal
222    /// is written in. A glyph or symbol terminal is drawn as its own cell, not in this stroke.
223    pub stroke: Stroke,
224}
225
226impl Shape for Connector {
227    /// Draws the two terminals and then the route, in that order. The order of the two terminals
228    /// is the order the endpoints are named, `from` then `to`, and it is load-bearing where two
229    /// endpoints share one position: under `Above` the second is the one seen.
230    ///
231    /// The route is derived from the two positions and the two leaving directions alone, so it is
232    /// called exactly as it always was and a terminal cannot reach it.
233    fn draw(&self, surface: &mut dyn Surface) {
234        for endpoint in [&self.from, &self.to] {
235            endpoint.draw(&self.stroke, surface);
236        }
237
238        if let Some(positions) =
239            derive_path(self.from.at, self.from.leaving, self.to.at, self.to.leaving)
240        {
241            Route {
242                from: self.from.at,
243                to: self.to.at,
244                positions,
245                stroke: self.stroke.clone(),
246            }
247            .draw(surface);
248        }
249    }
250}
251
252/// One step from `at` in `dir`, or `None` if that would overflow `i16`.
253fn offset(at: Pos, dir: Direction) -> Option<Pos> {
254    match dir {
255        Direction::Up => at.y.checked_sub(1).map(|y| Pos { x: at.x, y }),
256        Direction::Down => at.y.checked_add(1).map(|y| Pos { x: at.x, y }),
257        Direction::Left => at.x.checked_sub(1).map(|x| Pos { x, y: at.y }),
258        Direction::Right => at.x.checked_add(1).map(|x| Pos { x, y: at.y }),
259    }
260}
261
262fn opposite(dir: Direction) -> Direction {
263    match dir {
264        Direction::Up => Direction::Down,
265        Direction::Down => Direction::Up,
266        Direction::Left => Direction::Right,
267        Direction::Right => Direction::Left,
268    }
269}
270
271/// Every position from `from` to `to` inclusive, in that order. The two must share exactly one
272/// coordinate.
273fn positions_between(from: Pos, to: Pos) -> Vec<Pos> {
274    if from.x == to.x {
275        let (lo, hi) = (from.y.min(to.y), from.y.max(to.y));
276        let mut positions: Vec<Pos> = (lo..=hi).map(|y| Pos { x: from.x, y }).collect();
277        if from.y > to.y {
278            positions.reverse();
279        }
280        positions
281    } else {
282        let (lo, hi) = (from.x.min(to.x), from.x.max(to.x));
283        let mut positions: Vec<Pos> = (lo..=hi).map(|x| Pos { x, y: from.y }).collect();
284        if from.x > to.x {
285            positions.reverse();
286        }
287        positions
288    }
289}
290
291/// Every position between the waypoints of a chosen candidate, each pair's shared boundary point
292/// written once.
293fn expand_waypoints(waypoints: &[Pos]) -> Vec<Pos> {
294    if waypoints.len() == 1 {
295        return vec![waypoints[0]];
296    }
297
298    let mut positions = Vec::new();
299    for pair in waypoints.windows(2) {
300        let segment = positions_between(pair[0], pair[1]);
301        if positions.is_empty() {
302            positions.extend(segment);
303        } else {
304            positions.extend(segment.into_iter().skip(1));
305        }
306    }
307    positions
308}
309
310/// The smallest rectangle containing both starting positions. It bounds nothing; it is the span
311/// term 3 of the ranking measures its middle within — the module's _Design notes_.
312struct RouteRectangle {
313    x_min: i16,
314    x_max: i16,
315    y_min: i16,
316    y_max: i16,
317}
318
319impl RouteRectangle {
320    fn spanning(s: Pos, t: Pos) -> Self {
321        Self {
322            x_min: s.x.min(t.x),
323            x_max: s.x.max(t.x),
324            y_min: s.y.min(t.y),
325            y_max: s.y.max(t.y),
326        }
327    }
328
329    /// One value per axis: the cell halfway along that axis's span, taken as the one nearer
330    /// `anchor` — the `from` endpoint's starting position — where the span holds an even number
331    /// of cells and the halfway point falls between two — term 3 of the ranking, and why it
332    /// rounds that way, in the module's _Design notes_.
333    fn middle(&self, anchor: Pos) -> Pos {
334        Pos {
335            x: Self::midpoint(self.x_min, self.x_max, anchor.x),
336            y: Self::midpoint(self.y_min, self.y_max, anchor.y),
337        }
338    }
339
340    /// The cell halfway between `min` and `max` (inclusive), nearer `anchor` — which is always
341    /// one of the two, since it is one of the two positions the rectangle spans.
342    fn midpoint(min: i16, max: i16, anchor: i16) -> i16 {
343        let half = (max - min) / 2;
344        if anchor == min {
345            min + half
346        } else {
347            max - half
348        }
349    }
350}
351
352/// The orientation a direction moves along: `Left`/`Right` are horizontal, `Up`/`Down` vertical.
353fn direction_orientation(dir: Direction) -> Orientation {
354    match dir {
355        Direction::Left | Direction::Right => Orientation::Horizontal,
356        Direction::Up | Direction::Down => Orientation::Vertical,
357    }
358}
359
360/// The ranking of a route as one value — fewest bends, then shortest, then nearest the middle,
361/// then to the right of the travel — compared lexicographically in the fields' declared order.
362/// Deriving `Ord` over them **is** the ranking; nothing here is a written comparator.
363/// Why each term is there: the module's _Design notes_.
364#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
365struct Cost {
366    bends: u16,
367    length: u16,
368    from_middle: u16,
369    hand: u16,
370}
371
372/// A fixed order over the four directions, used only to index arrays and hash-map keys —
373/// `Direction` derives no `Hash`, and this module stays the one place that needs one.
374const DIRECTIONS: [Direction; 4] = [
375    Direction::Up,
376    Direction::Right,
377    Direction::Down,
378    Direction::Left,
379];
380
381fn direction_index(d: Direction) -> usize {
382    DIRECTIONS
383        .iter()
384        .position(|&candidate| candidate == d)
385        .expect("DIRECTIONS lists all four directions")
386}
387
388/// The lines a route may turn on, one per axis: the line each starting position pins, the line
389/// beside each of them, the middle, and one line outside the route rectangle on either side —
390/// deduplicated to at most 7 distinct lines. A node is a pair of indices into
391/// `x` and `y`, so at most 7 x 7 x 4 = 196 states whatever the arrangement, because
392/// a route turns only here — a claim the module's _Design notes_ call confirmed rather than
393/// proved, and what to re-run when a term is added to [`Cost`].
394struct Lattice {
395    x: Vec<i16>,
396    y: Vec<i16>,
397}
398
399/// A node of [`Lattice`], paired with the index into [`DIRECTIONS`] of the heading a run reaching
400/// it travels along.
401type State = ((usize, usize), usize);
402
403impl Lattice {
404    fn new(s: Pos, t: Pos, mid: Pos) -> Self {
405        Self {
406            x: Self::axis(s.x, t.x, mid.x),
407            y: Self::axis(s.y, t.y, mid.y),
408        }
409    }
410
411    fn axis(s_c: i16, t_c: i16, mid_c: i16) -> Vec<i16> {
412        let mut values = vec![s_c - 1, s_c, s_c + 1, mid_c, t_c - 1, t_c, t_c + 1];
413        values.sort_unstable();
414        values.dedup();
415        values
416    }
417
418    fn index_of(values: &[i16], value: i16) -> usize {
419        values
420            .binary_search(&value)
421            .expect("value is one of axis's own candidates")
422    }
423
424    fn node(&self, pos: Pos) -> (usize, usize) {
425        (
426            Self::index_of(&self.x, pos.x),
427            Self::index_of(&self.y, pos.y),
428        )
429    }
430
431    fn pos(&self, node: (usize, usize)) -> Pos {
432        Pos {
433            x: self.x[node.0],
434            y: self.y[node.1],
435        }
436    }
437
438    /// One step from `node` along `d`, or `None` at the lattice's own edge.
439    fn step(&self, node: (usize, usize), d: Direction) -> Option<(usize, usize)> {
440        let (ix, iy) = node;
441        match d {
442            Direction::Up => iy.checked_sub(1).map(|iy| (ix, iy)),
443            Direction::Down => (iy + 1 < self.y.len()).then_some((ix, iy + 1)),
444            Direction::Left => ix.checked_sub(1).map(|ix| (ix, iy)),
445            Direction::Right => (ix + 1 < self.x.len()).then_some((ix + 1, iy)),
446        }
447    }
448}
449
450/// Whether the coordinate `c` sits on the side leaving `da` puts to the travel's right, of
451/// `mid_c` — term 4 of the ranking, in the module's _Design notes_. Coordinates grow rightward
452/// and downward, so leaving `Up` or `Right` puts the larger coordinate to the right, and leaving
453/// `Down` or `Left` the smaller.
454fn is_right_of_travel(da: Direction, c: i16, mid_c: i16) -> bool {
455    match da {
456        Direction::Up | Direction::Right => c >= mid_c,
457        Direction::Down | Direction::Left => c <= mid_c,
458    }
459}
460
461/// The `from_middle` and `hand` a run of `orientation`, sitting at `pos` — any point along it,
462/// since a run's own fixed coordinate does not change — charges against `mid` and the connector's own
463/// travel `da`. `hand` is charged only where `orientation` matches `da`'s own, which is what makes
464/// term 4's handedness the connector's own travel rather than the coordinate system's.
465fn run_cost(orientation: Orientation, pos: Pos, mid: Pos, da: Direction) -> (u16, u16) {
466    let (c, mid_c) = match orientation {
467        Orientation::Horizontal => (pos.y, mid.y),
468        Orientation::Vertical => (pos.x, mid.x),
469    };
470    let from_middle = c.abs_diff(mid_c);
471    let hand = if orientation == direction_orientation(da) {
472        u16::from(!is_right_of_travel(da, c, mid_c))
473    } else {
474        0
475    };
476    (from_middle, hand)
477}
478
479/// Derives a connector's route: the path between its two starting positions, per _The route of a
480/// connector_. `None` means no path exists and the connector is its two heads alone.
481///
482/// A single-source Dijkstra over states `(node, heading)`, `node` a point of [`Lattice`] and
483/// `heading` the direction the run reaching it travels along, minimized by [`Cost`] — nothing
484/// bounds where the search goes beyond the lattice itself. The search starts
485/// at `s` with a virtual heading `da`, as though the route already arrived there leaving `a`, and
486/// ends at `t` against a virtual `exit_dir`, the direction continuing from `t` into `b`; both
487/// virtual steps charge a bend exactly where a real one would, and forbid reversing. `from_middle`
488/// and `hand` are charged to a run exactly when a bend closes it *and* a bend opened it — an
489/// interior run always opened with one, and closes with the one being charged now; the run
490/// leaving `s` opened with one only if its first direction is not `da`, which is exactly when
491/// `cost.bends` is still nonzero for the run about to close.
492fn derive_path(a: Pos, da: Direction, b: Pos, db: Direction) -> Option<Vec<Pos>> {
493    // Two endpoints at one position leaving the same direction: `s` and `t` coincide and the
494    // only way to arrive would be to leave immediately in the opposite direction, which
495    // _The route of a connector_ declines as a path returning to where it began. A search without
496    // the full path in its state cannot see that a longer alternative loops back over itself, so
497    // this is ruled out directly rather than left to the search to discover.
498    if a == b && da == db {
499        return None;
500    }
501    // `s` and `t` — each endpoint's starting position: one step from it in that endpoint's own
502    // leaving direction.
503    let s = offset(a, da)?;
504    let t = offset(b, db)?;
505    let exit_dir = opposite(db);
506    // No route cell is an endpoint position: an invariant the derivation below relies on. `a` and `b`
507    // are always
508    // lattice nodes — `a`'s coordinate on `da`'s axis is `s`'s own minus or plus one, and its
509    // other coordinate is `s`'s own, both always lattice candidates — so excluding them from the
510    // graph excludes every path that would cross a head.
511    let forbidden = |p: Pos| p == a || p == b;
512    if forbidden(s) || forbidden(t) {
513        return None;
514    }
515
516    let rectangle = RouteRectangle::spanning(s, t);
517    let mid = rectangle.middle(s);
518    let lattice = Lattice::new(s, t, mid);
519    let start = (lattice.node(s), direction_index(da));
520    let goal_node = lattice.node(t);
521
522    let mut dist: HashMap<State, Cost> = HashMap::new();
523    let mut prev: HashMap<State, State> = HashMap::new();
524    let mut heap = BinaryHeap::new();
525    dist.insert(start, Cost::default());
526    heap.push(Reverse((Cost::default(), start)));
527
528    while let Some(Reverse((cost, key))) = heap.pop() {
529        if dist.get(&key) != Some(&cost) {
530            continue;
531        }
532        let (node, heading_index) = key;
533        let heading = DIRECTIONS[heading_index];
534        let pos = lattice.pos(node);
535
536        for d in DIRECTIONS {
537            if d == opposite(heading) {
538                continue;
539            }
540            let Some(neighbor) = lattice.step(node, d) else {
541                continue;
542            };
543            let neighbor_pos = lattice.pos(neighbor);
544            if forbidden(neighbor_pos) {
545                continue;
546            }
547
548            let mut next = cost;
549            next.length += match direction_orientation(d) {
550                Orientation::Horizontal => neighbor_pos.x.abs_diff(pos.x),
551                Orientation::Vertical => neighbor_pos.y.abs_diff(pos.y),
552            };
553            if d != heading {
554                next.bends += 1;
555                if cost.bends > 0 {
556                    let (from_middle, hand) =
557                        run_cost(direction_orientation(heading), pos, mid, da);
558                    next.from_middle += from_middle;
559                    next.hand += hand;
560                }
561            }
562
563            let next_key = (neighbor, direction_index(d));
564            if dist.get(&next_key).is_none_or(|&best| next < best) {
565                dist.insert(next_key, next);
566                prev.insert(next_key, key);
567                heap.push(Reverse((next, next_key)));
568            }
569        }
570    }
571
572    let (_, winner) = DIRECTIONS
573        .into_iter()
574        .filter(|&h| h != opposite(exit_dir))
575        .filter_map(|h| {
576            let key = (goal_node, direction_index(h));
577            let cost = *dist.get(&key)?;
578            let mut total = cost;
579            if h != exit_dir {
580                total.bends += 1;
581                if cost.bends > 0 {
582                    let (from_middle, hand) = run_cost(direction_orientation(h), t, mid, da);
583                    total.from_middle += from_middle;
584                    total.hand += hand;
585                }
586            }
587            Some((total, key))
588        })
589        .min_by_key(|(total, _)| *total)?;
590
591    let mut path = Vec::new();
592    let mut current = winner;
593    loop {
594        path.push(lattice.pos(current.0));
595        if current == start {
596            break;
597        }
598        current = prev[&current];
599    }
600    path.reverse();
601    Some(expand_waypoints(&path))
602}
603
604#[cfg(test)]
605mod tests {
606    use super::{Connector, Endpoint, Lattice, RouteRectangle, Terminal, derive_path, offset};
607    use crate::shape::counting::CountingSurface;
608    use crate::{
609        Arm, Buffer, Cell, CellReader, Direction, FigureLayer, Glyph, GlyphCatalog, Line,
610        Orientation, Pos, Shape, Size, StampMode, Stroke, StrokeCell, Symbol, SymbolCell,
611        SymbolStyleName, Window, render,
612    };
613
614    fn light() -> Stroke {
615        Stroke::from("light")
616    }
617
618    /// The head glyph a picture-asserting test fixes: opposite the leaving direction.
619    fn head_for(leaving: Direction) -> Glyph {
620        let text = match leaving {
621            Direction::Up => "▼",
622            Direction::Down => "▲",
623            Direction::Left => "►",
624            Direction::Right => "◄",
625        };
626        Glyph::new(text).expect("one of the four head glyphs is one glyph each")
627    }
628
629    fn endpoint(at: Pos, leaving: Direction) -> Endpoint {
630        Endpoint {
631            at,
632            leaving,
633            terminal: Terminal::Glyph {
634                glyph: head_for(leaving),
635            },
636        }
637    }
638
639    fn render_connector(origin: Pos, size: Size, from: Endpoint, to: Endpoint) -> String {
640        render_connector_with(&GlyphCatalog::light(), origin, size, from, to)
641    }
642
643    fn render_connector_with(
644        glyphs: &GlyphCatalog,
645        origin: Pos,
646        size: Size,
647        from: Endpoint,
648        to: Endpoint,
649    ) -> String {
650        let mut buffer = Buffer::new(Window { origin, size });
651        Connector {
652            from,
653            to,
654            stroke: light(),
655        }
656        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
657
658        render(&buffer, glyphs, buffer.window())
659    }
660
661    /// Perpendicular directions, one bend.
662    #[test]
663    fn scenario_1_perpendicular_one_bend() {
664        let text = render_connector(
665            Pos { x: 0, y: 0 },
666            Size {
667                width: 5,
668                height: 4,
669            },
670            endpoint(Pos { x: 0, y: 0 }, Direction::Down),
671            endpoint(Pos { x: 4, y: 3 }, Direction::Left),
672        );
673
674        assert_eq!(text, "▲    \n│    \n│    \n└───►\n");
675    }
676
677    /// The mirror of scenario 1, and scenario 3 asserts the two differ.
678    #[test]
679    fn scenario_2_perpendicular_one_bend_mirrored() {
680        let text = render_connector(
681            Pos { x: 0, y: 0 },
682            Size {
683                width: 5,
684                height: 4,
685            },
686            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
687            endpoint(Pos { x: 4, y: 3 }, Direction::Up),
688        );
689
690        assert_eq!(text, "◄───┐\n    │\n    │\n    ▼\n");
691    }
692
693    /// Scenarios 1 and 2 share both endpoint positions and
694    /// differ only in direction, so their rendered texts must differ.
695    #[test]
696    fn scenario_3_direction_is_part_of_the_description() {
697        let size = Size {
698            width: 5,
699            height: 4,
700        };
701        let origin = Pos { x: 0, y: 0 };
702        let one = render_connector(
703            origin,
704            size,
705            endpoint(Pos { x: 0, y: 0 }, Direction::Down),
706            endpoint(Pos { x: 4, y: 3 }, Direction::Left),
707        );
708        let two = render_connector(
709            origin,
710            size,
711            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
712            endpoint(Pos { x: 4, y: 3 }, Direction::Up),
713        );
714
715        assert_ne!(one, two);
716    }
717
718    /// Opposite directions, endpoints aligned — a straight run, no
719    /// bend.
720    #[test]
721    fn scenario_4_opposite_aligned_no_bend() {
722        let text = render_connector(
723            Pos { x: 0, y: 0 },
724            Size {
725                width: 7,
726                height: 1,
727            },
728            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
729            endpoint(Pos { x: 6, y: 0 }, Direction::Left),
730        );
731
732        assert_eq!(text, "◄─────►\n");
733    }
734
735    /// Opposite directions, endpoints not aligned — two bends, the
736    /// pinned tie-break winner among three same-length candidates.
737    #[test]
738    fn scenario_5_opposite_not_aligned_two_bends() {
739        let text = render_connector(
740            Pos { x: 0, y: 0 },
741            Size {
742                width: 7,
743                height: 3,
744            },
745            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
746            endpoint(Pos { x: 6, y: 2 }, Direction::Left),
747        );
748
749        assert_eq!(text, "◄──┐   \n   │   \n   └──►\n");
750    }
751
752    /// The same two-bend family, entirely inside the endpoint
753    /// rectangle since both directions head toward the other end.
754    #[test]
755    fn scenario_6_two_bends_inside_the_endpoint_rectangle() {
756        let text = render_connector(
757            Pos { x: 0, y: 0 },
758            Size {
759                width: 9,
760                height: 3,
761            },
762            endpoint(Pos { x: 2, y: 0 }, Direction::Right),
763            endpoint(Pos { x: 8, y: 2 }, Direction::Left),
764        );
765
766        assert_eq!(text, "  ◄──┐   \n     │   \n     └──►\n");
767    }
768
769    /// Opposite directions, both endpoints facing away — four bends,
770    /// the route rectangle one cell wider on each side.
771    #[test]
772    fn scenario_7_facing_away_four_bends() {
773        let text = render_connector(
774            Pos { x: 0, y: 0 },
775            Size {
776                width: 10,
777                height: 3,
778            },
779            endpoint(Pos { x: 2, y: 0 }, Direction::Left),
780            endpoint(Pos { x: 8, y: 2 }, Direction::Right),
781        );
782
783        assert_eq!(text, " ┌►       \n └───────┐\n        ◄┘\n");
784    }
785
786    /// Perpendicular directions, one endpoint heading away — the route
787    /// rectangle grows on that one side only.
788    #[test]
789    fn scenario_8_perpendicular_one_heading_away() {
790        let text = render_connector(
791            Pos { x: 0, y: 0 },
792            Size {
793                width: 9,
794                height: 4,
795            },
796            endpoint(Pos { x: 2, y: 0 }, Direction::Left),
797            endpoint(Pos { x: 8, y: 2 }, Direction::Down),
798        );
799
800        assert_eq!(text, " ┌►      \n │       \n │      ▲\n └──────┘\n");
801    }
802
803    /// The tightest two-bend arrangement, where the two starting
804    /// positions share a column and the two bends are adjacent.
805    #[test]
806    fn scenario_9_tightest_two_bend_arrangement() {
807        let text = render_connector(
808            Pos { x: 0, y: 0 },
809            Size {
810                width: 5,
811                height: 2,
812            },
813            endpoint(Pos { x: 2, y: 0 }, Direction::Right),
814            endpoint(Pos { x: 4, y: 1 }, Direction::Left),
815        );
816
817        assert_eq!(text, "  ◄┐ \n   └►\n");
818    }
819
820    /// The tightest arrangement of the facing-away family.
821    #[test]
822    fn scenario_10_tightest_facing_away_arrangement() {
823        let text = render_connector(
824            Pos { x: 0, y: 0 },
825            Size {
826                width: 5,
827                height: 3,
828            },
829            endpoint(Pos { x: 2, y: 0 }, Direction::Left),
830            endpoint(Pos { x: 3, y: 2 }, Direction::Right),
831        );
832
833        assert_eq!(text, " ┌►  \n └──┐\n   ◄┘\n");
834    }
835
836    /// The three connectors of scenarios 6, 7 and 8 share both
837    /// endpoint positions and differ only in direction, so all three rendered texts must differ.
838    #[test]
839    fn scenario_11_the_three_shared_endpoint_connectors_all_differ() {
840        let six = render_connector(
841            Pos { x: 0, y: 0 },
842            Size {
843                width: 9,
844                height: 3,
845            },
846            endpoint(Pos { x: 2, y: 0 }, Direction::Right),
847            endpoint(Pos { x: 8, y: 2 }, Direction::Left),
848        );
849        let seven = render_connector(
850            Pos { x: 0, y: 0 },
851            Size {
852                width: 9,
853                height: 3,
854            },
855            endpoint(Pos { x: 2, y: 0 }, Direction::Left),
856            endpoint(Pos { x: 8, y: 2 }, Direction::Right),
857        );
858        let eight = render_connector(
859            Pos { x: 0, y: 0 },
860            Size {
861                width: 9,
862                height: 4,
863            },
864            endpoint(Pos { x: 2, y: 0 }, Direction::Left),
865            endpoint(Pos { x: 8, y: 2 }, Direction::Down),
866        );
867
868        assert_ne!(six, seven);
869        assert_ne!(six, eight);
870        assert_ne!(seven, eight);
871    }
872
873    /// Identical directions, the geometry where a path fits: both endpoints leaving `Right`, one
874    /// heading toward the other end and one heading away. This row was produced by running the
875    /// code, not derived: it is deliberately not asserted anywhere else.
876    #[test]
877    fn identical_directions_where_a_path_fits() {
878        let text = render_connector(
879            Pos { x: 0, y: 0 },
880            Size {
881                width: 8,
882                height: 3,
883            },
884            endpoint(Pos { x: 2, y: 0 }, Direction::Right),
885            endpoint(Pos { x: 6, y: 2 }, Direction::Right),
886        );
887
888        assert_eq!(text, "  ◄────┐\n       │\n      ◄┘\n");
889    }
890
891    /// Identical directions, the geometry where the two
892    /// endpoints are in line on that axis: `s` and `t` sit on the same row with the `to`
893    /// endpoint's own head between them, so the straight run the old bound drew nothing for is
894    /// not a route the rule can draw either — it would cross a head. The ranking instead
895    /// routes around the outside, per _The route of a connector_.
896    #[test]
897    fn identical_directions_in_line_are_joined_around_the_outside() {
898        let text = render_connector(
899            Pos { x: 0, y: 0 },
900            Size {
901                width: 6,
902                height: 2,
903            },
904            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
905            endpoint(Pos { x: 4, y: 0 }, Direction::Right),
906        );
907
908        assert_eq!(text, "◄──┐◄┐\n   └─┘\n");
909    }
910
911    /// Two endpoints that coincide return normally — no error, no panic. What it draws is the
912    /// general rule's business and is deliberately not asserted here.
913    #[test]
914    fn both_endpoints_at_the_same_position_returns_normally() {
915        let _ = render_connector(
916            Pos { x: 0, y: 0 },
917            Size {
918                width: 1,
919                height: 1,
920            },
921            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
922            endpoint(Pos { x: 0, y: 0 }, Direction::Left),
923        );
924    }
925
926    /// None of the ten pinned connectors writes any position more than
927    /// once — including where a route bends, which is where a route drawn as two overlapping
928    /// runs would write twice.
929    #[test]
930    fn no_pinned_connector_writes_any_position_more_than_once() {
931        let pairs = [
932            (
933                endpoint(Pos { x: 0, y: 0 }, Direction::Down),
934                endpoint(Pos { x: 4, y: 3 }, Direction::Left),
935            ),
936            (
937                endpoint(Pos { x: 0, y: 0 }, Direction::Right),
938                endpoint(Pos { x: 4, y: 3 }, Direction::Up),
939            ),
940            (
941                endpoint(Pos { x: 0, y: 0 }, Direction::Right),
942                endpoint(Pos { x: 6, y: 0 }, Direction::Left),
943            ),
944            (
945                endpoint(Pos { x: 0, y: 0 }, Direction::Right),
946                endpoint(Pos { x: 6, y: 2 }, Direction::Left),
947            ),
948            (
949                endpoint(Pos { x: 2, y: 0 }, Direction::Right),
950                endpoint(Pos { x: 8, y: 2 }, Direction::Left),
951            ),
952            (
953                endpoint(Pos { x: 2, y: 0 }, Direction::Left),
954                endpoint(Pos { x: 8, y: 2 }, Direction::Right),
955            ),
956            (
957                endpoint(Pos { x: 2, y: 0 }, Direction::Left),
958                endpoint(Pos { x: 8, y: 2 }, Direction::Down),
959            ),
960            (
961                endpoint(Pos { x: 2, y: 0 }, Direction::Right),
962                endpoint(Pos { x: 4, y: 1 }, Direction::Left),
963            ),
964            (
965                endpoint(Pos { x: 2, y: 0 }, Direction::Left),
966                endpoint(Pos { x: 3, y: 2 }, Direction::Right),
967            ),
968        ];
969
970        for (index, (from, to)) in pairs.into_iter().enumerate() {
971            let mut surface = CountingSurface::default();
972            Connector {
973                from,
974                to,
975                stroke: light(),
976            }
977            .draw(&mut surface);
978
979            assert!(
980                surface.max_writes() <= 1,
981                "pinned connector {} wrote a position more than once",
982                index + 1
983            );
984        }
985    }
986
987    /// The bug report's connector renders the same picture whichever endpoint is named
988    /// first, instead of drawing the route backwards out of its starting cell.
989    #[test]
990    fn the_bug_report_renders_the_same_from_either_end() {
991        let origin = Pos { x: 0, y: 0 };
992        let size = Size {
993            width: 7,
994            height: 1,
995        };
996        let expected = "◄─────►\n";
997
998        let named_left_first = render_connector(
999            origin,
1000            size,
1001            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1002            endpoint(Pos { x: 6, y: 0 }, Direction::Left),
1003        );
1004        let named_right_first = render_connector(
1005            origin,
1006            size,
1007            endpoint(Pos { x: 6, y: 0 }, Direction::Left),
1008            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1009        );
1010
1011        assert_eq!(named_left_first, expected);
1012        assert_eq!(named_right_first, expected);
1013    }
1014
1015    /// A pair whose free coordinate spans an even number of cells, so the two orders
1016    /// draw different — and equally correct — pictures, turning at row 3 or row 4
1017    /// depending on which endpoint is named first.
1018    #[test]
1019    fn an_even_free_span_turns_at_the_row_nearer_the_endpoint_named_first() {
1020        let origin = Pos { x: 0, y: 0 };
1021        let size = Size {
1022            width: 3,
1023            height: 7,
1024        };
1025
1026        let up_first = render_connector(
1027            origin,
1028            size,
1029            endpoint(Pos { x: 0, y: 1 }, Direction::Down),
1030            endpoint(Pos { x: 2, y: 6 }, Direction::Up),
1031        );
1032        let down_first = render_connector(
1033            origin,
1034            size,
1035            endpoint(Pos { x: 2, y: 6 }, Direction::Up),
1036            endpoint(Pos { x: 0, y: 1 }, Direction::Down),
1037        );
1038
1039        assert_eq!(
1040            up_first,
1041            concat!(
1042                "   \n",
1043                "▲  \n",
1044                "│  \n",
1045                "└─┐\n",
1046                "  │\n",
1047                "  │\n",
1048                "  ▼\n",
1049            )
1050        );
1051        assert_eq!(
1052            down_first,
1053            concat!(
1054                "   \n",
1055                "▲  \n",
1056                "│  \n",
1057                "│  \n",
1058                "└─┐\n",
1059                "  │\n",
1060                "  ▼\n",
1061            )
1062        );
1063    }
1064
1065    /// Each arrangement turns at the middle of its route rectangle
1066    /// rather than at an edge (`n = 4`) or at neither of the two middle cells (`n = 5`).
1067    /// `n = 5` names the far endpoint first so the even-span tie-break, taken nearer the endpoint
1068    /// the connector leaves from, picks the cell this table pins, `x = 3`.
1069    #[test]
1070    fn each_arrangement_turns_at_the_middle_of_its_route_rectangle() {
1071        let origin = Pos { x: 0, y: 0 };
1072
1073        let n4 = render_connector(
1074            origin,
1075            Size {
1076                width: 5,
1077                height: 4,
1078            },
1079            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1080            endpoint(Pos { x: 4, y: 3 }, Direction::Left),
1081        );
1082        let n5 = render_connector(
1083            origin,
1084            Size {
1085                width: 6,
1086                height: 4,
1087            },
1088            endpoint(Pos { x: 5, y: 3 }, Direction::Left),
1089            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1090        );
1091        let n6 = render_connector(
1092            origin,
1093            Size {
1094                width: 7,
1095                height: 4,
1096            },
1097            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1098            endpoint(Pos { x: 6, y: 3 }, Direction::Left),
1099        );
1100
1101        assert_eq!(n4, concat!("◄─┐  \n", "  │  \n", "  │  \n", "  └─►\n"));
1102        assert_eq!(n5, concat!("◄──┐  \n", "   │  \n", "   │  \n", "   └─►\n"));
1103        assert_eq!(
1104            n6,
1105            concat!("◄──┐   \n", "   │   \n", "   │   \n", "   └──►\n")
1106        );
1107    }
1108
1109    /// Where the coordinate the bends leave free spans exactly two cells, the route
1110    /// turns at the cell nearer the endpoint the connector leaves from. Issue
1111    /// 104: today the `waypoints.len() > 3` proxy discards the winning shape for naming one point
1112    /// twice and the opposite corner wins instead, so both orders turn the wrong way round. This
1113    /// test MUST pass unchanged through the Dijkstra rewrite.
1114    #[test]
1115    fn a_two_cell_free_span_turns_toward_the_endpoint_the_connector_leaves_from() {
1116        let origin = Pos { x: 0, y: 0 };
1117        let size = Size {
1118            width: 4,
1119            height: 2,
1120        };
1121
1122        let leaving_right_first = render_connector(
1123            origin,
1124            size,
1125            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1126            endpoint(Pos { x: 3, y: 1 }, Direction::Left),
1127        );
1128        let leaving_left_first = render_connector(
1129            origin,
1130            size,
1131            endpoint(Pos { x: 3, y: 1 }, Direction::Left),
1132            endpoint(Pos { x: 0, y: 0 }, Direction::Right),
1133        );
1134
1135        assert_eq!(leaving_right_first, concat!("◄┐  \n", " └─►\n"));
1136        assert_eq!(leaving_left_first, concat!("◄─┐ \n", "  └►\n"));
1137    }
1138
1139    /// The shipped demonstration's connector turns at the middle of its route
1140    /// rectangle. The middle is computed here from the two endpoint positions rather
1141    /// than transcribed from a picture, which is what would have caught this defect had it
1142    /// existed in the demonstration.
1143    #[test]
1144    fn the_demonstration_turns_at_the_middle_of_its_route_rectangle() {
1145        let a = Pos { x: 13, y: 3 };
1146        let da = Direction::Right;
1147        let b = Pos { x: 22, y: 4 };
1148        let db = Direction::Down;
1149
1150        let s = offset(a, da).expect("no overflow in this fixture");
1151        let t = offset(b, db).expect("no overflow in this fixture");
1152        let middle_x = i16::midpoint(s.x.min(t.x), s.x.max(t.x));
1153        assert_eq!(middle_x, 18);
1154
1155        let path = derive_path(a, da, b, db).expect("the demonstration's connector has a route");
1156        assert!(path.contains(&Pos {
1157            x: middle_x,
1158            y: s.y
1159        }));
1160        assert!(path.contains(&Pos {
1161            x: middle_x,
1162            y: t.y
1163        }));
1164    }
1165
1166    /// Where both endpoints occupy one position, the glyph seen is the `to`
1167    /// endpoint's head — `Connector` draws `from` then `to`, and `Above` lets the second win. Pins
1168    /// the behavior; does not change it.
1169    #[test]
1170    fn c6_the_to_head_wins_a_shared_cell() {
1171        let text = render_connector(
1172            Pos { x: 2, y: 1 },
1173            Size {
1174                width: 1,
1175                height: 1,
1176            },
1177            endpoint(Pos { x: 2, y: 1 }, Direction::Right),
1178            endpoint(Pos { x: 2, y: 1 }, Direction::Left),
1179        );
1180
1181        assert_eq!(text, "►\n");
1182    }
1183
1184    /// A glyph terminal writes a literal, and
1185    /// a literal is decided on every side — so nothing composes into the cell the terminal hangs
1186    /// from. An arm written at that same position afterwards leaves the cell exactly as the
1187    /// terminal wrote it, which is the one difference from an arm terminal this test's line makes
1188    /// visible.
1189    #[test]
1190    fn a_glyph_terminals_cell_is_decided_and_nothing_composes_into_it() {
1191        let origin = Pos { x: 0, y: 0 };
1192        let size = Size {
1193            width: 7,
1194            height: 1,
1195        };
1196        let at = origin;
1197        let mut buffer = Buffer::new(Window { origin, size });
1198
1199        Connector {
1200            from: Endpoint {
1201                at,
1202                leaving: Direction::Right,
1203                terminal: Terminal::Glyph {
1204                    glyph: Glyph::new("◄").expect("one glyph"),
1205                },
1206            },
1207            to: endpoint(Pos { x: 6, y: 0 }, Direction::Left),
1208            stroke: light(),
1209        }
1210        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
1211
1212        // A line's first cell is an end carrying one arm, so this writes an arm on the same
1213        // position the glyph terminal occupies and nothing else.
1214        Line {
1215            at,
1216            len: 1,
1217            orientation: Orientation::Horizontal,
1218            stroke: light(),
1219        }
1220        .draw(&mut FigureLayer::new(&mut buffer, StampMode::Below));
1221
1222        assert_eq!(
1223            buffer.cell(at),
1224            Some(&Cell::Literal(head_for(Direction::Right)))
1225        );
1226    }
1227
1228    /// An arm terminal writes one arm — the
1229    /// one the connector leaves on — in the connector's own stroke, and leaves the other three sides
1230    /// undecided so that whatever reaches the cell afterwards may still join it. All four leaving
1231    /// directions, because the side is derived from the direction and a mapping that happens to be
1232    /// right for three of the four is wrong.
1233    #[test]
1234    fn an_arm_terminal_writes_one_arm_on_the_side_leaving_names_and_unset_on_the_other_three() {
1235        let expected = |leaving: Direction| {
1236            let (top, right, bottom, left) = match leaving {
1237                Direction::Up => (true, false, false, false),
1238                Direction::Right => (false, true, false, false),
1239                Direction::Down => (false, false, true, false),
1240                Direction::Left => (false, false, false, true),
1241            };
1242            let arm = |set: bool| {
1243                if set { Arm::Set(light()) } else { Arm::Unset }
1244            };
1245            Cell::from(StrokeCell {
1246                base: light(),
1247                top: arm(top),
1248                right: arm(right),
1249                bottom: arm(bottom),
1250                left: arm(left),
1251            })
1252        };
1253
1254        for leaving in [
1255            Direction::Up,
1256            Direction::Right,
1257            Direction::Down,
1258            Direction::Left,
1259        ] {
1260            // A one-by-one window on the endpoint's own position, so nothing but the terminal
1261            // writes there: the route starts one step away and the other endpoint is off-window.
1262            let at = Pos { x: 3, y: 3 };
1263            let mut buffer = Buffer::new(Window {
1264                origin: at,
1265                size: Size {
1266                    width: 1,
1267                    height: 1,
1268                },
1269            });
1270            Connector {
1271                from: Endpoint {
1272                    at,
1273                    leaving,
1274                    terminal: Terminal::Arm,
1275                },
1276                to: endpoint(Pos { x: 9, y: 9 }, Direction::Up),
1277                stroke: light(),
1278            }
1279            .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
1280
1281            assert_eq!(
1282                buffer.cell(at),
1283                Some(&expected(leaving)),
1284                "leaving {leaving:?}"
1285            );
1286        }
1287    }
1288
1289    /// A symbol terminal writes one named symbol — the one pointing against `leaving` — in the
1290    /// style the caller named. All four leaving directions, because the direction is derived from
1291    /// `leaving` and a mapping that happens to be right for three of the four is wrong. The pair
1292    /// is written as it was named: the buffer holds a `Cell::Symbol`, and the catalog answers it
1293    /// later, at render.
1294    #[test]
1295    fn a_symbol_terminal_writes_the_symbol_pointing_against_leaving() {
1296        let style = SymbolStyleName::from("solid");
1297        let expected = |leaving: Direction| match leaving {
1298            Direction::Up => Symbol::DirectionDown,
1299            Direction::Right => Symbol::DirectionLeft,
1300            Direction::Down => Symbol::DirectionUp,
1301            Direction::Left => Symbol::DirectionRight,
1302        };
1303
1304        for leaving in [
1305            Direction::Up,
1306            Direction::Right,
1307            Direction::Down,
1308            Direction::Left,
1309        ] {
1310            // A one-by-one window on the endpoint's own position, so nothing but the terminal
1311            // writes there: the route starts one step away and the other endpoint is off-window.
1312            let at = Pos { x: 3, y: 3 };
1313            let mut buffer = Buffer::new(Window {
1314                origin: at,
1315                size: Size {
1316                    width: 1,
1317                    height: 1,
1318                },
1319            });
1320            Connector {
1321                from: Endpoint {
1322                    at,
1323                    leaving,
1324                    terminal: Terminal::Symbol {
1325                        style: style.clone(),
1326                    },
1327                },
1328                to: endpoint(Pos { x: 9, y: 9 }, Direction::Up),
1329                stroke: light(),
1330            }
1331            .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
1332
1333            assert_eq!(
1334                buffer.cell(at),
1335                Some(&Cell::Symbol(SymbolCell {
1336                    symbol: expected(leaving),
1337                    style: style.clone(),
1338                })),
1339                "leaving {leaving:?}"
1340            );
1341        }
1342    }
1343
1344    /// The symbol terminal draws the head the model's convention prescribes without the caller
1345    /// spelling it, so a connector whose terminals name `solid` renders what one whose terminals
1346    /// carry the heads by hand renders — the routes are the same cells either way, and only the
1347    /// endpoint cells are written by the set rather than by the caller. The catalog holds both
1348    /// tables, which is the point: the head comes from the style and the body from the connector's
1349    /// stroke. `solid` draws triangles, so the spelled side uses `◀` and `▶` where [`head_for`] —
1350    /// the pointer style the other pictures fix — would use `◄` and `►`.
1351    #[test]
1352    fn a_symbol_terminal_draws_the_head_a_glyph_terminal_spells_out() {
1353        let origin = Pos { x: 0, y: 0 };
1354        let size = Size {
1355            width: 7,
1356            height: 3,
1357        };
1358        let glyphs = GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid()]);
1359        let solid_head = |leaving: Direction| {
1360            Glyph::new(match leaving {
1361                Direction::Up => "▼",
1362                Direction::Right => "◀",
1363                Direction::Down => "▲",
1364                Direction::Left => "▶",
1365            })
1366            .expect("one of the four solid heads is one glyph each")
1367        };
1368        let spelled = render_connector_with(
1369            &glyphs,
1370            origin,
1371            size,
1372            Endpoint {
1373                at: Pos { x: 0, y: 0 },
1374                leaving: Direction::Right,
1375                terminal: Terminal::Glyph {
1376                    glyph: solid_head(Direction::Right),
1377                },
1378            },
1379            Endpoint {
1380                at: Pos { x: 6, y: 2 },
1381                leaving: Direction::Left,
1382                terminal: Terminal::Glyph {
1383                    glyph: solid_head(Direction::Left),
1384                },
1385            },
1386        );
1387        let named = render_connector_with(
1388            &glyphs,
1389            origin,
1390            size,
1391            Endpoint {
1392                at: Pos { x: 0, y: 0 },
1393                leaving: Direction::Right,
1394                terminal: Terminal::Symbol {
1395                    style: SymbolStyleName::from("solid"),
1396                },
1397            },
1398            Endpoint {
1399                at: Pos { x: 6, y: 2 },
1400                leaving: Direction::Left,
1401                terminal: Terminal::Symbol {
1402                    style: SymbolStyleName::from("solid"),
1403                },
1404            },
1405        );
1406
1407        assert_eq!(named, spelled);
1408        assert_eq!(named, "◀──┐   \n   │   \n   └──▶\n");
1409    }
1410
1411    /// D2's case: the terminal is a field of the endpoint, so one connector may carry two styles —
1412    /// the `from` end names `solid` and the `to` end `arrow`, and each head comes from its own
1413    /// table. A style at one end and a glyph or an arm at the other follows from the same field.
1414    #[test]
1415    fn the_two_endpoints_of_a_connector_name_their_own_styles() {
1416        let arrow = GlyphCatalog::from_symbol_rules([(
1417            SymbolCell {
1418                symbol: Symbol::DirectionRight,
1419                style: SymbolStyleName::from("arrow"),
1420            },
1421            Glyph::new("→").expect("one glyph"),
1422        )]);
1423        let glyphs = GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid(), arrow]);
1424        let text = render_connector_with(
1425            &glyphs,
1426            Pos { x: 0, y: 0 },
1427            Size {
1428                width: 7,
1429                height: 1,
1430            },
1431            Endpoint {
1432                at: Pos { x: 0, y: 0 },
1433                leaving: Direction::Right,
1434                terminal: Terminal::Symbol {
1435                    style: SymbolStyleName::from("solid"),
1436                },
1437            },
1438            Endpoint {
1439                at: Pos { x: 6, y: 0 },
1440                leaving: Direction::Left,
1441                terminal: Terminal::Symbol {
1442                    style: SymbolStyleName::from("arrow"),
1443                },
1444            },
1445        );
1446
1447        assert_eq!(text, "◀─────→\n");
1448    }
1449
1450    /// The route is derived from the two positions and
1451    /// the two leaving directions alone, so a terminal cannot reach it and the body is the same
1452    /// cells either way. Checked position by position rather than by looking at two pictures,
1453    /// because the claim is that the texts differ at two named positions and nowhere else.
1454    #[test]
1455    fn the_two_terminals_write_the_same_body_and_differ_only_at_the_endpoints() {
1456        let origin = Pos { x: 0, y: 0 };
1457        let size = Size {
1458            width: 11,
1459            height: 3,
1460        };
1461        let from_at = Pos { x: 2, y: 1 };
1462        let to_at = Pos { x: 8, y: 1 };
1463
1464        let glyph = |text: &str| Terminal::Glyph {
1465            glyph: Glyph::new(text).expect("one glyph"),
1466        };
1467        let with_glyphs = render_connector(
1468            origin,
1469            size,
1470            Endpoint {
1471                at: from_at,
1472                leaving: Direction::Right,
1473                terminal: glyph("◄"),
1474            },
1475            Endpoint {
1476                at: to_at,
1477                leaving: Direction::Left,
1478                terminal: glyph("►"),
1479            },
1480        );
1481        let with_arms = render_connector(
1482            origin,
1483            size,
1484            Endpoint {
1485                at: from_at,
1486                leaving: Direction::Right,
1487                terminal: Terminal::Arm,
1488            },
1489            Endpoint {
1490                at: to_at,
1491                leaving: Direction::Left,
1492                terminal: Terminal::Arm,
1493            },
1494        );
1495
1496        let char_at = |text: &str, at: Pos| {
1497            let column = |value: i16| {
1498                value
1499                    .checked_sub(origin.x)
1500                    .and_then(|offset| usize::try_from(offset).ok())
1501            };
1502            let row = |value: i16| {
1503                value
1504                    .checked_sub(origin.y)
1505                    .and_then(|offset| usize::try_from(offset).ok())
1506            };
1507            match (column(at.x), row(at.y)) {
1508                (Some(column), Some(row)) => text
1509                    .lines()
1510                    .nth(row)
1511                    .and_then(|line| line.chars().nth(column)),
1512                _ => None,
1513            }
1514        };
1515        let positions: Vec<Pos> = (0..size.height)
1516            .flat_map(|y| {
1517                (0..size.width).map(move |x| Pos {
1518                    x: origin.x + i16::try_from(x).expect("a window width fits i16"),
1519                    y: origin.y + i16::try_from(y).expect("a window height fits i16"),
1520                })
1521            })
1522            .collect();
1523        let differing: Vec<Pos> = positions
1524            .into_iter()
1525            .filter(|at| char_at(&with_glyphs, *at) != char_at(&with_arms, *at))
1526            .collect();
1527
1528        assert_eq!(differing, vec![from_at, to_at]);
1529    }
1530
1531    /// The path writes no endpoint cell
1532    /// whatever the terminal is, so the terminal is the only thing an endpoint contributes to the
1533    /// cell it names.
1534    ///
1535    /// All three terminals are run over the same arrangement in turn, and the route is not empty —
1536    /// the five cells between the endpoints are asserted written — so a largest write count of one
1537    /// is not vacuous. Were the path to write an endpoint cell, that cell would carry the
1538    /// terminal's write and the path's, and the count there would be two.
1539    #[test]
1540    fn the_path_writes_no_endpoint_cell_whichever_terminal_is_used() {
1541        let origin = Pos { x: 0, y: 0 };
1542        let size = Size {
1543            width: 11,
1544            height: 3,
1545        };
1546        let from_at = Pos { x: 2, y: 1 };
1547        let to_at = Pos { x: 8, y: 1 };
1548        let body: Vec<Pos> = (3..=7).map(|x| Pos { x, y: 1 }).collect();
1549
1550        for terminal in [
1551            Terminal::Glyph {
1552                glyph: Glyph::new("◄").expect("one glyph"),
1553            },
1554            Terminal::Symbol {
1555                style: SymbolStyleName::from("solid"),
1556            },
1557            Terminal::Arm,
1558        ] {
1559            let connector = Connector {
1560                from: Endpoint {
1561                    at: from_at,
1562                    leaving: Direction::Right,
1563                    terminal: terminal.clone(),
1564                },
1565                to: Endpoint {
1566                    at: to_at,
1567                    leaving: Direction::Left,
1568                    terminal: terminal.clone(),
1569                },
1570                stroke: light(),
1571            };
1572
1573            let mut buffer = Buffer::new(Window { origin, size });
1574            connector.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
1575            for at in body.iter().copied().chain([from_at, to_at]) {
1576                assert!(
1577                    buffer.cell(at).is_some(),
1578                    "{terminal:?} wrote nothing at {at:?}"
1579                );
1580            }
1581
1582            let mut counting = CountingSurface::default();
1583            connector.draw(&mut counting);
1584            assert_eq!(
1585                counting.max_writes(),
1586                1,
1587                "{terminal:?} wrote some position more than once"
1588            );
1589        }
1590    }
1591
1592    /// Two endpoints facing away
1593    /// from each other on one line are joined, wrapping around the outside on the side the
1594    /// travel puts to its right, however far apart the two are — the same shape with longer runs.
1595    #[test]
1596    fn facing_away_are_joined_at_any_distance() {
1597        let near = render_connector(
1598            Pos { x: 0, y: -1 },
1599            Size {
1600                width: 2,
1601                height: 5,
1602            },
1603            endpoint(Pos { x: 0, y: 0 }, Direction::Up),
1604            endpoint(Pos { x: 0, y: 2 }, Direction::Down),
1605        );
1606        let far = render_connector(
1607            Pos { x: 0, y: -1 },
1608            Size {
1609                width: 2,
1610                height: 21,
1611            },
1612            endpoint(Pos { x: 0, y: 0 }, Direction::Up),
1613            endpoint(Pos { x: 0, y: 18 }, Direction::Down),
1614        );
1615
1616        assert_eq!(near, "┌┐\n▼│\n │\n▲│\n└┘\n");
1617        assert_eq!(far, format!("┌┐\n▼│\n{}▲│\n└┘\n", " │\n".repeat(17)));
1618    }
1619
1620    /// The same family with the two
1621    /// starting positions on different columns takes the same four bends around the outside.
1622    #[test]
1623    fn facing_away_off_column_takes_four_bends_around_the_outside() {
1624        let text = render_connector(
1625            Pos { x: -1, y: -1 },
1626            Size {
1627                width: 3,
1628                height: 5,
1629            },
1630            endpoint(Pos { x: 0, y: 0 }, Direction::Up),
1631            endpoint(Pos { x: 1, y: 2 }, Direction::Down),
1632        );
1633
1634        assert_eq!(text, "┌┐ \n│▼ \n│  \n│ ▲\n└─┘\n");
1635    }
1636
1637    /// The former double escape
1638    /// takes four bends rather than six, and the two orders mirror each other rather than drawing
1639    /// the same picture — `zigzag` is gone.
1640    #[test]
1641    fn the_former_double_escape_takes_four_bends_and_the_two_orders_mirror() {
1642        let size = Size {
1643            width: 5,
1644            height: 3,
1645        };
1646
1647        let leaving_left_first = render_connector(
1648            Pos { x: -1, y: -1 },
1649            size,
1650            endpoint(Pos { x: 0, y: 0 }, Direction::Left),
1651            endpoint(Pos { x: 2, y: 1 }, Direction::Right),
1652        );
1653        let leaving_right_first = render_connector(
1654            Pos { x: -1, y: 0 },
1655            size,
1656            endpoint(Pos { x: 2, y: 1 }, Direction::Right),
1657            endpoint(Pos { x: 0, y: 0 }, Direction::Left),
1658        );
1659
1660        assert_eq!(leaving_left_first, "┌───┐\n└►  │\n   ◄┘\n");
1661        assert_eq!(leaving_right_first, "┌►   \n│  ◄┐\n└───┘\n");
1662
1663        let bends = |text: &str| text.chars().filter(|c| "┌┐└┘".contains(*c)).count();
1664        assert_eq!(bends(&leaving_left_first), 4);
1665        assert_eq!(bends(&leaving_right_first), 4);
1666    }
1667
1668    /// Where two routes mirror
1669    /// each other about the line the two starting positions share — here, `b`'s own head stands
1670    /// between them, so neither can go straight through — the one drawn is the one on the side
1671    /// the connector's own travel puts to its right. Leaving `down`, that is the smaller `x`.
1672    #[test]
1673    fn a_mirrored_route_passes_on_the_right_of_the_travel() {
1674        let text = render_connector(
1675            Pos { x: -1, y: 0 },
1676            Size {
1677                width: 2,
1678                height: 4,
1679            },
1680            endpoint(Pos { x: 0, y: 0 }, Direction::Down),
1681            endpoint(Pos { x: 0, y: 2 }, Direction::Down),
1682        );
1683
1684        assert_eq!(text, " ▲\n┌┘\n│▲\n└┘\n");
1685    }
1686
1687    /// The route is empty in exactly the two arrangements the model names —
1688    /// an endpoint standing on the cell the route would have to arrive at, and two endpoints at
1689    /// one position leaving the same direction.
1690    #[test]
1691    fn the_route_is_empty_only_where_the_model_says_it_is() {
1692        let an_endpoint_stands_on_the_arrival_cell = derive_path(
1693            Pos { x: 0, y: 0 },
1694            Direction::Up,
1695            Pos { x: 0, y: 1 },
1696            Direction::Up,
1697        );
1698        let coincident_leaving_the_same_direction = derive_path(
1699            Pos { x: 0, y: 0 },
1700            Direction::Right,
1701            Pos { x: 0, y: 0 },
1702            Direction::Right,
1703        );
1704
1705        assert_eq!(an_endpoint_stands_on_the_arrival_cell, None);
1706        assert_eq!(coincident_leaving_the_same_direction, None);
1707    }
1708
1709    /// Of the sixteen arrangements whose two endpoints sit at one position —
1710    /// excluded from the sweep grid, so this is the only thing that covers them — the four that
1711    /// leave in the same direction draw no route, and the twelve that leave in different
1712    /// directions draw one.
1713    #[test]
1714    fn the_coincident_position_family_follows_the_rule() {
1715        const DIRECTIONS: [Direction; 4] = [
1716            Direction::Up,
1717            Direction::Right,
1718            Direction::Down,
1719            Direction::Left,
1720        ];
1721        let at = Pos { x: 0, y: 0 };
1722
1723        let mut same_direction = 0;
1724        let mut different_direction = 0;
1725        for da in DIRECTIONS {
1726            for db in DIRECTIONS {
1727                let route = derive_path(at, da, at, db);
1728                if da == db {
1729                    assert_eq!(route, None, "leaving {da:?} both ways draws no route");
1730                    same_direction += 1;
1731                } else {
1732                    assert!(route.is_some(), "leaving {da:?} then {db:?} draws a route");
1733                    different_direction += 1;
1734                }
1735            }
1736        }
1737
1738        assert_eq!(same_direction, 4);
1739        assert_eq!(different_direction, 12);
1740    }
1741
1742    /// The number of states the derivation searches — the lattice's nodes,
1743    /// times the four headings — is equal for two endpoints four, fifty and five hundred cells
1744    /// apart, and never exceeds `7 x 7 x 4 = 196`. Counted directly from [`Lattice`] rather than
1745    /// timed, and rather than counting `expand_waypoints` or `Route::draw`, which grow with the
1746    /// route's length by design.
1747    #[test]
1748    fn the_state_count_does_not_grow_with_distance() {
1749        let states_apart = |cells_apart: i16| {
1750            let s = Pos { x: 0, y: -1 };
1751            let t = Pos {
1752                x: 0,
1753                y: cells_apart + 1,
1754            };
1755            let mid = RouteRectangle::spanning(s, t).middle(s);
1756            let lattice = Lattice::new(s, t, mid);
1757            lattice.x.len() * lattice.y.len() * 4
1758        };
1759
1760        let four = states_apart(4);
1761        let fifty = states_apart(50);
1762        let five_hundred = states_apart(500);
1763
1764        assert_eq!(four, fifty);
1765        assert_eq!(fifty, five_hundred);
1766        assert!(four <= 196);
1767    }
1768
1769    /// The window the sweep renders into, sized to hold the six-by-five field
1770    /// plus the one cell of margin a leaving direction can add on each side.
1771    const SWEEP_ORIGIN: Pos = Pos { x: -2, y: -2 };
1772    const SWEEP_SIZE: Size = Size {
1773        width: 10,
1774        height: 9,
1775    };
1776
1777    /// One group of [`sweep_arrangements_by_anchor`]: the anchor, its leaving direction, and
1778    /// every `(position, leaving direction)` the anchor is paired with.
1779    type AnchorGroup = (Pos, Direction, Vec<(Pos, Direction)>);
1780
1781    /// The sweep's grid, grouped by anchor and its leaving direction: two anchors,
1782    /// each leaving in four directions — eight groups of 116 arrangements each, against every
1783    /// position of a six-by-five field with four leaving directions each, excluding the
1784    /// arrangements where the second position is the anchor itself. The grouping is what lets the
1785    /// sweep's snapshot split into one file per group instead of one no review tool can render.
1786    fn sweep_arrangements_by_anchor() -> Vec<AnchorGroup> {
1787        const DIRECTIONS: [Direction; 4] = [
1788            Direction::Up,
1789            Direction::Right,
1790            Direction::Down,
1791            Direction::Left,
1792        ];
1793        let anchors = [Pos { x: 0, y: 0 }, Pos { x: 2, y: 1 }];
1794
1795        let mut groups = Vec::new();
1796        for anchor in anchors {
1797            for anchor_dir in DIRECTIONS {
1798                let mut others = Vec::new();
1799                for x in 0..6 {
1800                    for y in 0..5 {
1801                        let other = Pos { x, y };
1802                        if other == anchor {
1803                            continue;
1804                        }
1805                        for other_dir in DIRECTIONS {
1806                            others.push((other, other_dir));
1807                        }
1808                    }
1809                }
1810                groups.push((anchor, anchor_dir, others));
1811            }
1812        }
1813        groups
1814    }
1815
1816    /// Whether `p` falls inside the sweep's window — the same inclusion rule as
1817    /// [`crate::Buffer::contains`], duplicated here because that method is private and this test
1818    /// asserts against positions the derivation returns, not against a buffer.
1819    fn window_contains(p: Pos) -> bool {
1820        let dx =
1821            p.x.checked_sub(SWEEP_ORIGIN.x)
1822                .and_then(|d| u16::try_from(d).ok());
1823        let dy =
1824            p.y.checked_sub(SWEEP_ORIGIN.y)
1825                .and_then(|d| u16::try_from(d).ok());
1826        matches!((dx, dy), (Some(dx), Some(dy)) if dx < SWEEP_SIZE.width && dy < SWEEP_SIZE.height)
1827    }
1828
1829    /// Every position any sweep arrangement writes — both heads and every cell of
1830    /// the derived route — lies inside the window the sweep renders into, so a rendering showing
1831    /// two heads with a gap can only mean an empty route rather than a clipped one. The window is
1832    /// that wide because the lattice a route may turn on never reaches more than one line outside
1833    /// the rectangle the two starting positions span, which this assertion checks mechanically
1834    /// rather than by that arithmetic alone.
1835    #[test]
1836    fn sweep_every_route_fits_inside_the_window() {
1837        for (a, da, b, db) in sweep_arrangements() {
1838            for (from_at, from_dir, to_at, to_dir) in [(a, da, b, db), (b, db, a, da)] {
1839                assert!(
1840                    window_contains(from_at) && window_contains(to_at),
1841                    "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): a head outside the window"
1842                );
1843                if let Some(path) = derive_path(from_at, from_dir, to_at, to_dir) {
1844                    for pos in path {
1845                        assert!(
1846                            window_contains(pos),
1847                            "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): route cell {pos:?} outside the window"
1848                        );
1849                    }
1850                }
1851            }
1852        }
1853    }
1854
1855    /// The sweep's grid, flattened — 928 arrangements.
1856    fn sweep_arrangements() -> Vec<(Pos, Direction, Pos, Direction)> {
1857        sweep_arrangements_by_anchor()
1858            .into_iter()
1859            .flat_map(|(anchor, anchor_dir, others)| {
1860                others
1861                    .into_iter()
1862                    .map(move |(other, other_dir)| (anchor, anchor_dir, other, other_dir))
1863            })
1864            .collect()
1865    }
1866
1867    /// The sweep's own check on its grid definition: it reproduces 928.
1868    #[test]
1869    fn the_sweep_grid_has_928_arrangements() {
1870        assert_eq!(sweep_arrangements().len(), 928);
1871    }
1872
1873    /// Over the whole grid, rendered from both ends: both endpoint
1874    /// positions always render their own head, and drawing into a surface that counts writes
1875    /// never writes any position more than once.
1876    #[test]
1877    fn sweep_every_endpoint_renders_its_own_head_and_no_position_is_written_twice() {
1878        for (a, da, b, db) in sweep_arrangements() {
1879            for (from_at, from_dir, to_at, to_dir) in [(a, da, b, db), (b, db, a, da)] {
1880                let mut buffer = Buffer::new(Window {
1881                    origin: SWEEP_ORIGIN,
1882                    size: SWEEP_SIZE,
1883                });
1884                Connector {
1885                    from: endpoint(from_at, from_dir),
1886                    to: endpoint(to_at, to_dir),
1887                    stroke: light(),
1888                }
1889                .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
1890                assert_eq!(
1891                    buffer.cell(from_at),
1892                    Some(&Cell::Literal(head_for(from_dir))),
1893                    "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): from's own head"
1894                );
1895                assert_eq!(
1896                    buffer.cell(to_at),
1897                    Some(&Cell::Literal(head_for(to_dir))),
1898                    "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): to's own head"
1899                );
1900
1901                let mut counting = CountingSurface::default();
1902                Connector {
1903                    from: endpoint(from_at, from_dir),
1904                    to: endpoint(to_at, to_dir),
1905                    stroke: light(),
1906                }
1907                .draw(&mut counting);
1908                assert!(
1909                    counting.max_writes() <= 1,
1910                    "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}) wrote a position more than once"
1911                );
1912            }
1913        }
1914    }
1915
1916    /// The whole grid, rendered from both ends and labeled by arrangement, pinned as one
1917    /// file per anchor and leaving direction — eight rather than one, so a PR review tool can
1918    /// render each diff; a single 20,000-line file is what GitHub would not show at all.
1919    /// These eight are a characterization, so they live apart from every picture a test asserts by
1920    /// hand and are accepted on a report of what moved — how many cases, in which families of
1921    /// arrangement, and three examples with their before and after — rather than on a claim of
1922    /// review that nobody can keep; the file states this at its own head.
1923    /// Each line's trailing blanks are trimmed before it goes into the snapshot, since
1924    /// the gate's `editorconfig-checker` step runs with `trim_trailing_whitespace` on
1925    /// and `render` pads every line to the window's width.
1926    #[test]
1927    fn sweep_matches_the_pinned_characterization() {
1928        use std::fmt::Write as _;
1929
1930        let mut settings = insta::Settings::clone_current();
1931        settings.set_snapshot_path(concat!(
1932            env!("CARGO_MANIFEST_DIR"),
1933            "/src/snapshots/characterization"
1934        ));
1935        settings.set_description(concat!(
1936            "This file records what the code does over a range too wide to assert by hand. ",
1937            "A change here is the consequence of a decision taken elsewhere, not a decision. ",
1938            "It is accepted after a report of how many cases moved, in which families, and ",
1939            "three examples with their before and after.",
1940        ));
1941        settings.bind(|| {
1942            for (anchor, anchor_dir, others) in sweep_arrangements_by_anchor() {
1943                let mut rendered = String::new();
1944                for (other, other_dir) in others {
1945                    for (from_at, from_dir, to_at, to_dir) in [
1946                        (anchor, anchor_dir, other, other_dir),
1947                        (other, other_dir, anchor, anchor_dir),
1948                    ] {
1949                        let text = render_connector(
1950                            SWEEP_ORIGIN,
1951                            SWEEP_SIZE,
1952                            endpoint(from_at, from_dir),
1953                            endpoint(to_at, to_dir),
1954                        );
1955                        let trimmed: Vec<&str> = text.lines().map(str::trim_end).collect();
1956                        let _ = writeln!(
1957                            rendered,
1958                            "({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?})\n{}\n",
1959                            trimmed.join("\n")
1960                        );
1961                    }
1962                }
1963                let name = format!(
1964                    "connector_sweep_anchor_{}_{}_{anchor_dir:?}",
1965                    anchor.x, anchor.y
1966                )
1967                .to_lowercase();
1968                insta::assert_snapshot!(name, rendered);
1969            }
1970        });
1971    }
1972}