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[¤t];
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}