use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
use crate::cell::Side;
use crate::shape::fragment::end::End;
use crate::shape::fragment::literal::Literal;
use crate::shape::fragment::symbol::NamedSymbol;
use crate::shape::route::Route;
use crate::{
Direction, Glyph, Orientation, Pos, Shape, Stroke, Surface, Symbol, SymbolCell, SymbolStyleName,
};
#[derive(Debug)]
pub struct Endpoint {
pub at: Pos,
pub leaving: Direction,
pub terminal: Terminal,
}
impl Endpoint {
fn draw(&self, stroke: &Stroke, surface: &mut dyn Surface) {
match &self.terminal {
Terminal::Glyph { glyph } => Literal {
at: self.at,
glyph: glyph.clone(),
}
.draw(surface),
Terminal::Symbol { style } => NamedSymbol {
at: self.at,
cell: SymbolCell {
symbol: symbol_for(self.leaving),
style: style.clone(),
},
}
.draw(surface),
Terminal::Arm => End {
at: self.at,
side: arm_side(self.leaving),
stroke: stroke.clone(),
}
.draw(surface),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Terminal {
Glyph {
glyph: Glyph,
},
Symbol {
style: SymbolStyleName,
},
Arm,
}
fn arm_side(leaving: Direction) -> Side {
match leaving {
Direction::Up => Side::Top,
Direction::Right => Side::Right,
Direction::Down => Side::Bottom,
Direction::Left => Side::Left,
}
}
fn pointing(dir: Direction) -> Symbol {
match dir {
Direction::Up => Symbol::DirectionUp,
Direction::Right => Symbol::DirectionRight,
Direction::Down => Symbol::DirectionDown,
Direction::Left => Symbol::DirectionLeft,
}
}
fn symbol_for(leaving: Direction) -> Symbol {
pointing(opposite(leaving))
}
#[derive(Debug)]
pub struct Connector {
pub from: Endpoint,
pub to: Endpoint,
pub stroke: Stroke,
}
impl Shape for Connector {
fn draw(&self, surface: &mut dyn Surface) {
for endpoint in [&self.from, &self.to] {
endpoint.draw(&self.stroke, surface);
}
if let Some(positions) =
derive_path(self.from.at, self.from.leaving, self.to.at, self.to.leaving)
{
Route {
from: self.from.at,
to: self.to.at,
positions,
stroke: self.stroke.clone(),
}
.draw(surface);
}
}
}
fn offset(at: Pos, dir: Direction) -> Option<Pos> {
match dir {
Direction::Up => at.y.checked_sub(1).map(|y| Pos { x: at.x, y }),
Direction::Down => at.y.checked_add(1).map(|y| Pos { x: at.x, y }),
Direction::Left => at.x.checked_sub(1).map(|x| Pos { x, y: at.y }),
Direction::Right => at.x.checked_add(1).map(|x| Pos { x, y: at.y }),
}
}
fn opposite(dir: Direction) -> Direction {
match dir {
Direction::Up => Direction::Down,
Direction::Down => Direction::Up,
Direction::Left => Direction::Right,
Direction::Right => Direction::Left,
}
}
fn positions_between(from: Pos, to: Pos) -> Vec<Pos> {
if from.x == to.x {
let (lo, hi) = (from.y.min(to.y), from.y.max(to.y));
let mut positions: Vec<Pos> = (lo..=hi).map(|y| Pos { x: from.x, y }).collect();
if from.y > to.y {
positions.reverse();
}
positions
} else {
let (lo, hi) = (from.x.min(to.x), from.x.max(to.x));
let mut positions: Vec<Pos> = (lo..=hi).map(|x| Pos { x, y: from.y }).collect();
if from.x > to.x {
positions.reverse();
}
positions
}
}
fn expand_waypoints(waypoints: &[Pos]) -> Vec<Pos> {
if waypoints.len() == 1 {
return vec![waypoints[0]];
}
let mut positions = Vec::new();
for pair in waypoints.windows(2) {
let segment = positions_between(pair[0], pair[1]);
if positions.is_empty() {
positions.extend(segment);
} else {
positions.extend(segment.into_iter().skip(1));
}
}
positions
}
struct RouteRectangle {
x_min: i16,
x_max: i16,
y_min: i16,
y_max: i16,
}
impl RouteRectangle {
fn spanning(s: Pos, t: Pos) -> Self {
Self {
x_min: s.x.min(t.x),
x_max: s.x.max(t.x),
y_min: s.y.min(t.y),
y_max: s.y.max(t.y),
}
}
fn middle(&self, anchor: Pos) -> Pos {
Pos {
x: Self::midpoint(self.x_min, self.x_max, anchor.x),
y: Self::midpoint(self.y_min, self.y_max, anchor.y),
}
}
fn midpoint(min: i16, max: i16, anchor: i16) -> i16 {
let half = (max - min) / 2;
if anchor == min {
min + half
} else {
max - half
}
}
}
fn direction_orientation(dir: Direction) -> Orientation {
match dir {
Direction::Left | Direction::Right => Orientation::Horizontal,
Direction::Up | Direction::Down => Orientation::Vertical,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Cost {
bends: u16,
length: u16,
from_middle: u16,
hand: u16,
}
const DIRECTIONS: [Direction; 4] = [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
];
fn direction_index(d: Direction) -> usize {
DIRECTIONS
.iter()
.position(|&candidate| candidate == d)
.expect("DIRECTIONS lists all four directions")
}
struct Lattice {
x: Vec<i16>,
y: Vec<i16>,
}
type State = ((usize, usize), usize);
impl Lattice {
fn new(s: Pos, t: Pos, mid: Pos) -> Self {
Self {
x: Self::axis(s.x, t.x, mid.x),
y: Self::axis(s.y, t.y, mid.y),
}
}
fn axis(s_c: i16, t_c: i16, mid_c: i16) -> Vec<i16> {
let mut values = vec![s_c - 1, s_c, s_c + 1, mid_c, t_c - 1, t_c, t_c + 1];
values.sort_unstable();
values.dedup();
values
}
fn index_of(values: &[i16], value: i16) -> usize {
values
.binary_search(&value)
.expect("value is one of axis's own candidates")
}
fn node(&self, pos: Pos) -> (usize, usize) {
(
Self::index_of(&self.x, pos.x),
Self::index_of(&self.y, pos.y),
)
}
fn pos(&self, node: (usize, usize)) -> Pos {
Pos {
x: self.x[node.0],
y: self.y[node.1],
}
}
fn step(&self, node: (usize, usize), d: Direction) -> Option<(usize, usize)> {
let (ix, iy) = node;
match d {
Direction::Up => iy.checked_sub(1).map(|iy| (ix, iy)),
Direction::Down => (iy + 1 < self.y.len()).then_some((ix, iy + 1)),
Direction::Left => ix.checked_sub(1).map(|ix| (ix, iy)),
Direction::Right => (ix + 1 < self.x.len()).then_some((ix + 1, iy)),
}
}
}
fn is_right_of_travel(da: Direction, c: i16, mid_c: i16) -> bool {
match da {
Direction::Up | Direction::Right => c >= mid_c,
Direction::Down | Direction::Left => c <= mid_c,
}
}
fn run_cost(orientation: Orientation, pos: Pos, mid: Pos, da: Direction) -> (u16, u16) {
let (c, mid_c) = match orientation {
Orientation::Horizontal => (pos.y, mid.y),
Orientation::Vertical => (pos.x, mid.x),
};
let from_middle = c.abs_diff(mid_c);
let hand = if orientation == direction_orientation(da) {
u16::from(!is_right_of_travel(da, c, mid_c))
} else {
0
};
(from_middle, hand)
}
fn derive_path(a: Pos, da: Direction, b: Pos, db: Direction) -> Option<Vec<Pos>> {
if a == b && da == db {
return None;
}
let s = offset(a, da)?;
let t = offset(b, db)?;
let exit_dir = opposite(db);
let forbidden = |p: Pos| p == a || p == b;
if forbidden(s) || forbidden(t) {
return None;
}
let rectangle = RouteRectangle::spanning(s, t);
let mid = rectangle.middle(s);
let lattice = Lattice::new(s, t, mid);
let start = (lattice.node(s), direction_index(da));
let goal_node = lattice.node(t);
let mut dist: HashMap<State, Cost> = HashMap::new();
let mut prev: HashMap<State, State> = HashMap::new();
let mut heap = BinaryHeap::new();
dist.insert(start, Cost::default());
heap.push(Reverse((Cost::default(), start)));
while let Some(Reverse((cost, key))) = heap.pop() {
if dist.get(&key) != Some(&cost) {
continue;
}
let (node, heading_index) = key;
let heading = DIRECTIONS[heading_index];
let pos = lattice.pos(node);
for d in DIRECTIONS {
if d == opposite(heading) {
continue;
}
let Some(neighbor) = lattice.step(node, d) else {
continue;
};
let neighbor_pos = lattice.pos(neighbor);
if forbidden(neighbor_pos) {
continue;
}
let mut next = cost;
next.length += match direction_orientation(d) {
Orientation::Horizontal => neighbor_pos.x.abs_diff(pos.x),
Orientation::Vertical => neighbor_pos.y.abs_diff(pos.y),
};
if d != heading {
next.bends += 1;
if cost.bends > 0 {
let (from_middle, hand) =
run_cost(direction_orientation(heading), pos, mid, da);
next.from_middle += from_middle;
next.hand += hand;
}
}
let next_key = (neighbor, direction_index(d));
if dist.get(&next_key).is_none_or(|&best| next < best) {
dist.insert(next_key, next);
prev.insert(next_key, key);
heap.push(Reverse((next, next_key)));
}
}
}
let (_, winner) = DIRECTIONS
.into_iter()
.filter(|&h| h != opposite(exit_dir))
.filter_map(|h| {
let key = (goal_node, direction_index(h));
let cost = *dist.get(&key)?;
let mut total = cost;
if h != exit_dir {
total.bends += 1;
if cost.bends > 0 {
let (from_middle, hand) = run_cost(direction_orientation(h), t, mid, da);
total.from_middle += from_middle;
total.hand += hand;
}
}
Some((total, key))
})
.min_by_key(|(total, _)| *total)?;
let mut path = Vec::new();
let mut current = winner;
loop {
path.push(lattice.pos(current.0));
if current == start {
break;
}
current = prev[¤t];
}
path.reverse();
Some(expand_waypoints(&path))
}
#[cfg(test)]
mod tests {
use super::{Connector, Endpoint, Lattice, RouteRectangle, Terminal, derive_path, offset};
use crate::shape::counting::CountingSurface;
use crate::{
Arm, Buffer, Cell, CellReader, Direction, FigureLayer, Glyph, GlyphCatalog, Line,
Orientation, Pos, Shape, Size, StampMode, Stroke, StrokeCell, Symbol, SymbolCell,
SymbolStyleName, Window, render,
};
fn light() -> Stroke {
Stroke::from("light")
}
fn head_for(leaving: Direction) -> Glyph {
let text = match leaving {
Direction::Up => "▼",
Direction::Down => "▲",
Direction::Left => "►",
Direction::Right => "◄",
};
Glyph::new(text).expect("one of the four head glyphs is one glyph each")
}
fn endpoint(at: Pos, leaving: Direction) -> Endpoint {
Endpoint {
at,
leaving,
terminal: Terminal::Glyph {
glyph: head_for(leaving),
},
}
}
fn render_connector(origin: Pos, size: Size, from: Endpoint, to: Endpoint) -> String {
render_connector_with(&GlyphCatalog::light(), origin, size, from, to)
}
fn render_connector_with(
glyphs: &GlyphCatalog,
origin: Pos,
size: Size,
from: Endpoint,
to: Endpoint,
) -> String {
let mut buffer = Buffer::new(Window { origin, size });
Connector {
from,
to,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
render(&buffer, glyphs, buffer.window())
}
#[test]
fn scenario_1_perpendicular_one_bend() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 5,
height: 4,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Down),
endpoint(Pos { x: 4, y: 3 }, Direction::Left),
);
assert_eq!(text, "▲ \n│ \n│ \n└───►\n");
}
#[test]
fn scenario_2_perpendicular_one_bend_mirrored() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 5,
height: 4,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 3 }, Direction::Up),
);
assert_eq!(text, "◄───┐\n │\n │\n ▼\n");
}
#[test]
fn scenario_3_direction_is_part_of_the_description() {
let size = Size {
width: 5,
height: 4,
};
let origin = Pos { x: 0, y: 0 };
let one = render_connector(
origin,
size,
endpoint(Pos { x: 0, y: 0 }, Direction::Down),
endpoint(Pos { x: 4, y: 3 }, Direction::Left),
);
let two = render_connector(
origin,
size,
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 3 }, Direction::Up),
);
assert_ne!(one, two);
}
#[test]
fn scenario_4_opposite_aligned_no_bend() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 7,
height: 1,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 0 }, Direction::Left),
);
assert_eq!(text, "◄─────►\n");
}
#[test]
fn scenario_5_opposite_not_aligned_two_bends() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 7,
height: 3,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 2 }, Direction::Left),
);
assert_eq!(text, "◄──┐ \n │ \n └──►\n");
}
#[test]
fn scenario_6_two_bends_inside_the_endpoint_rectangle() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 9,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 8, y: 2 }, Direction::Left),
);
assert_eq!(text, " ◄──┐ \n │ \n └──►\n");
}
#[test]
fn scenario_7_facing_away_four_bends() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 10,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Right),
);
assert_eq!(text, " ┌► \n └───────┐\n ◄┘\n");
}
#[test]
fn scenario_8_perpendicular_one_heading_away() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 9,
height: 4,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Down),
);
assert_eq!(text, " ┌► \n │ \n │ ▲\n └──────┘\n");
}
#[test]
fn scenario_9_tightest_two_bend_arrangement() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 5,
height: 2,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 1 }, Direction::Left),
);
assert_eq!(text, " ◄┐ \n └►\n");
}
#[test]
fn scenario_10_tightest_facing_away_arrangement() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 5,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 3, y: 2 }, Direction::Right),
);
assert_eq!(text, " ┌► \n └──┐\n ◄┘\n");
}
#[test]
fn scenario_11_the_three_shared_endpoint_connectors_all_differ() {
let six = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 9,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 8, y: 2 }, Direction::Left),
);
let seven = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 9,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Right),
);
let eight = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 9,
height: 4,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Down),
);
assert_ne!(six, seven);
assert_ne!(six, eight);
assert_ne!(seven, eight);
}
#[test]
fn identical_directions_where_a_path_fits() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 8,
height: 3,
},
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 2 }, Direction::Right),
);
assert_eq!(text, " ◄────┐\n │\n ◄┘\n");
}
#[test]
fn identical_directions_in_line_are_joined_around_the_outside() {
let text = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 6,
height: 2,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 0 }, Direction::Right),
);
assert_eq!(text, "◄──┐◄┐\n └─┘\n");
}
#[test]
fn both_endpoints_at_the_same_position_returns_normally() {
let _ = render_connector(
Pos { x: 0, y: 0 },
Size {
width: 1,
height: 1,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 0, y: 0 }, Direction::Left),
);
}
#[test]
fn no_pinned_connector_writes_any_position_more_than_once() {
let pairs = [
(
endpoint(Pos { x: 0, y: 0 }, Direction::Down),
endpoint(Pos { x: 4, y: 3 }, Direction::Left),
),
(
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 3 }, Direction::Up),
),
(
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 0 }, Direction::Left),
),
(
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 2 }, Direction::Left),
),
(
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 8, y: 2 }, Direction::Left),
),
(
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Right),
),
(
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 8, y: 2 }, Direction::Down),
),
(
endpoint(Pos { x: 2, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 1 }, Direction::Left),
),
(
endpoint(Pos { x: 2, y: 0 }, Direction::Left),
endpoint(Pos { x: 3, y: 2 }, Direction::Right),
),
];
for (index, (from, to)) in pairs.into_iter().enumerate() {
let mut surface = CountingSurface::default();
Connector {
from,
to,
stroke: light(),
}
.draw(&mut surface);
assert!(
surface.max_writes() <= 1,
"pinned connector {} wrote a position more than once",
index + 1
);
}
}
#[test]
fn the_bug_report_renders_the_same_from_either_end() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 7,
height: 1,
};
let expected = "◄─────►\n";
let named_left_first = render_connector(
origin,
size,
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 0 }, Direction::Left),
);
let named_right_first = render_connector(
origin,
size,
endpoint(Pos { x: 6, y: 0 }, Direction::Left),
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
);
assert_eq!(named_left_first, expected);
assert_eq!(named_right_first, expected);
}
#[test]
fn an_even_free_span_turns_at_the_row_nearer_the_endpoint_named_first() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 3,
height: 7,
};
let up_first = render_connector(
origin,
size,
endpoint(Pos { x: 0, y: 1 }, Direction::Down),
endpoint(Pos { x: 2, y: 6 }, Direction::Up),
);
let down_first = render_connector(
origin,
size,
endpoint(Pos { x: 2, y: 6 }, Direction::Up),
endpoint(Pos { x: 0, y: 1 }, Direction::Down),
);
assert_eq!(
up_first,
concat!(
" \n",
"▲ \n",
"│ \n",
"└─┐\n",
" │\n",
" │\n",
" ▼\n",
)
);
assert_eq!(
down_first,
concat!(
" \n",
"▲ \n",
"│ \n",
"│ \n",
"└─┐\n",
" │\n",
" ▼\n",
)
);
}
#[test]
fn each_arrangement_turns_at_the_middle_of_its_route_rectangle() {
let origin = Pos { x: 0, y: 0 };
let n4 = render_connector(
origin,
Size {
width: 5,
height: 4,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 4, y: 3 }, Direction::Left),
);
let n5 = render_connector(
origin,
Size {
width: 6,
height: 4,
},
endpoint(Pos { x: 5, y: 3 }, Direction::Left),
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
);
let n6 = render_connector(
origin,
Size {
width: 7,
height: 4,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 6, y: 3 }, Direction::Left),
);
assert_eq!(n4, concat!("◄─┐ \n", " │ \n", " │ \n", " └─►\n"));
assert_eq!(n5, concat!("◄──┐ \n", " │ \n", " │ \n", " └─►\n"));
assert_eq!(
n6,
concat!("◄──┐ \n", " │ \n", " │ \n", " └──►\n")
);
}
#[test]
fn a_two_cell_free_span_turns_toward_the_endpoint_the_connector_leaves_from() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 4,
height: 2,
};
let leaving_right_first = render_connector(
origin,
size,
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
endpoint(Pos { x: 3, y: 1 }, Direction::Left),
);
let leaving_left_first = render_connector(
origin,
size,
endpoint(Pos { x: 3, y: 1 }, Direction::Left),
endpoint(Pos { x: 0, y: 0 }, Direction::Right),
);
assert_eq!(leaving_right_first, concat!("◄┐ \n", " └─►\n"));
assert_eq!(leaving_left_first, concat!("◄─┐ \n", " └►\n"));
}
#[test]
fn the_demonstration_turns_at_the_middle_of_its_route_rectangle() {
let a = Pos { x: 13, y: 3 };
let da = Direction::Right;
let b = Pos { x: 22, y: 4 };
let db = Direction::Down;
let s = offset(a, da).expect("no overflow in this fixture");
let t = offset(b, db).expect("no overflow in this fixture");
let middle_x = i16::midpoint(s.x.min(t.x), s.x.max(t.x));
assert_eq!(middle_x, 18);
let path = derive_path(a, da, b, db).expect("the demonstration's connector has a route");
assert!(path.contains(&Pos {
x: middle_x,
y: s.y
}));
assert!(path.contains(&Pos {
x: middle_x,
y: t.y
}));
}
#[test]
fn c6_the_to_head_wins_a_shared_cell() {
let text = render_connector(
Pos { x: 2, y: 1 },
Size {
width: 1,
height: 1,
},
endpoint(Pos { x: 2, y: 1 }, Direction::Right),
endpoint(Pos { x: 2, y: 1 }, Direction::Left),
);
assert_eq!(text, "►\n");
}
#[test]
fn a_glyph_terminals_cell_is_decided_and_nothing_composes_into_it() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 7,
height: 1,
};
let at = origin;
let mut buffer = Buffer::new(Window { origin, size });
Connector {
from: Endpoint {
at,
leaving: Direction::Right,
terminal: Terminal::Glyph {
glyph: Glyph::new("◄").expect("one glyph"),
},
},
to: endpoint(Pos { x: 6, y: 0 }, Direction::Left),
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
Line {
at,
len: 1,
orientation: Orientation::Horizontal,
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Below));
assert_eq!(
buffer.cell(at),
Some(&Cell::Literal(head_for(Direction::Right)))
);
}
#[test]
fn an_arm_terminal_writes_one_arm_on_the_side_leaving_names_and_unset_on_the_other_three() {
let expected = |leaving: Direction| {
let (top, right, bottom, left) = match leaving {
Direction::Up => (true, false, false, false),
Direction::Right => (false, true, false, false),
Direction::Down => (false, false, true, false),
Direction::Left => (false, false, false, true),
};
let arm = |set: bool| {
if set { Arm::Set(light()) } else { Arm::Unset }
};
Cell::from(StrokeCell {
base: light(),
top: arm(top),
right: arm(right),
bottom: arm(bottom),
left: arm(left),
})
};
for leaving in [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
] {
let at = Pos { x: 3, y: 3 };
let mut buffer = Buffer::new(Window {
origin: at,
size: Size {
width: 1,
height: 1,
},
});
Connector {
from: Endpoint {
at,
leaving,
terminal: Terminal::Arm,
},
to: endpoint(Pos { x: 9, y: 9 }, Direction::Up),
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
buffer.cell(at),
Some(&expected(leaving)),
"leaving {leaving:?}"
);
}
}
#[test]
fn a_symbol_terminal_writes_the_symbol_pointing_against_leaving() {
let style = SymbolStyleName::from("solid");
let expected = |leaving: Direction| match leaving {
Direction::Up => Symbol::DirectionDown,
Direction::Right => Symbol::DirectionLeft,
Direction::Down => Symbol::DirectionUp,
Direction::Left => Symbol::DirectionRight,
};
for leaving in [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
] {
let at = Pos { x: 3, y: 3 };
let mut buffer = Buffer::new(Window {
origin: at,
size: Size {
width: 1,
height: 1,
},
});
Connector {
from: Endpoint {
at,
leaving,
terminal: Terminal::Symbol {
style: style.clone(),
},
},
to: endpoint(Pos { x: 9, y: 9 }, Direction::Up),
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
buffer.cell(at),
Some(&Cell::Symbol(SymbolCell {
symbol: expected(leaving),
style: style.clone(),
})),
"leaving {leaving:?}"
);
}
}
#[test]
fn a_symbol_terminal_draws_the_head_a_glyph_terminal_spells_out() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 7,
height: 3,
};
let glyphs = GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid()]);
let solid_head = |leaving: Direction| {
Glyph::new(match leaving {
Direction::Up => "▼",
Direction::Right => "◀",
Direction::Down => "▲",
Direction::Left => "▶",
})
.expect("one of the four solid heads is one glyph each")
};
let spelled = render_connector_with(
&glyphs,
origin,
size,
Endpoint {
at: Pos { x: 0, y: 0 },
leaving: Direction::Right,
terminal: Terminal::Glyph {
glyph: solid_head(Direction::Right),
},
},
Endpoint {
at: Pos { x: 6, y: 2 },
leaving: Direction::Left,
terminal: Terminal::Glyph {
glyph: solid_head(Direction::Left),
},
},
);
let named = render_connector_with(
&glyphs,
origin,
size,
Endpoint {
at: Pos { x: 0, y: 0 },
leaving: Direction::Right,
terminal: Terminal::Symbol {
style: SymbolStyleName::from("solid"),
},
},
Endpoint {
at: Pos { x: 6, y: 2 },
leaving: Direction::Left,
terminal: Terminal::Symbol {
style: SymbolStyleName::from("solid"),
},
},
);
assert_eq!(named, spelled);
assert_eq!(named, "◀──┐ \n │ \n └──▶\n");
}
#[test]
fn the_two_endpoints_of_a_connector_name_their_own_styles() {
let arrow = GlyphCatalog::from_symbol_rules([(
SymbolCell {
symbol: Symbol::DirectionRight,
style: SymbolStyleName::from("arrow"),
},
Glyph::new("→").expect("one glyph"),
)]);
let glyphs = GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid(), arrow]);
let text = render_connector_with(
&glyphs,
Pos { x: 0, y: 0 },
Size {
width: 7,
height: 1,
},
Endpoint {
at: Pos { x: 0, y: 0 },
leaving: Direction::Right,
terminal: Terminal::Symbol {
style: SymbolStyleName::from("solid"),
},
},
Endpoint {
at: Pos { x: 6, y: 0 },
leaving: Direction::Left,
terminal: Terminal::Symbol {
style: SymbolStyleName::from("arrow"),
},
},
);
assert_eq!(text, "◀─────→\n");
}
#[test]
fn the_two_terminals_write_the_same_body_and_differ_only_at_the_endpoints() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 11,
height: 3,
};
let from_at = Pos { x: 2, y: 1 };
let to_at = Pos { x: 8, y: 1 };
let glyph = |text: &str| Terminal::Glyph {
glyph: Glyph::new(text).expect("one glyph"),
};
let with_glyphs = render_connector(
origin,
size,
Endpoint {
at: from_at,
leaving: Direction::Right,
terminal: glyph("◄"),
},
Endpoint {
at: to_at,
leaving: Direction::Left,
terminal: glyph("►"),
},
);
let with_arms = render_connector(
origin,
size,
Endpoint {
at: from_at,
leaving: Direction::Right,
terminal: Terminal::Arm,
},
Endpoint {
at: to_at,
leaving: Direction::Left,
terminal: Terminal::Arm,
},
);
let char_at = |text: &str, at: Pos| {
let column = |value: i16| {
value
.checked_sub(origin.x)
.and_then(|offset| usize::try_from(offset).ok())
};
let row = |value: i16| {
value
.checked_sub(origin.y)
.and_then(|offset| usize::try_from(offset).ok())
};
match (column(at.x), row(at.y)) {
(Some(column), Some(row)) => text
.lines()
.nth(row)
.and_then(|line| line.chars().nth(column)),
_ => None,
}
};
let positions: Vec<Pos> = (0..size.height)
.flat_map(|y| {
(0..size.width).map(move |x| Pos {
x: origin.x + i16::try_from(x).expect("a window width fits i16"),
y: origin.y + i16::try_from(y).expect("a window height fits i16"),
})
})
.collect();
let differing: Vec<Pos> = positions
.into_iter()
.filter(|at| char_at(&with_glyphs, *at) != char_at(&with_arms, *at))
.collect();
assert_eq!(differing, vec![from_at, to_at]);
}
#[test]
fn the_path_writes_no_endpoint_cell_whichever_terminal_is_used() {
let origin = Pos { x: 0, y: 0 };
let size = Size {
width: 11,
height: 3,
};
let from_at = Pos { x: 2, y: 1 };
let to_at = Pos { x: 8, y: 1 };
let body: Vec<Pos> = (3..=7).map(|x| Pos { x, y: 1 }).collect();
for terminal in [
Terminal::Glyph {
glyph: Glyph::new("◄").expect("one glyph"),
},
Terminal::Symbol {
style: SymbolStyleName::from("solid"),
},
Terminal::Arm,
] {
let connector = Connector {
from: Endpoint {
at: from_at,
leaving: Direction::Right,
terminal: terminal.clone(),
},
to: Endpoint {
at: to_at,
leaving: Direction::Left,
terminal: terminal.clone(),
},
stroke: light(),
};
let mut buffer = Buffer::new(Window { origin, size });
connector.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
for at in body.iter().copied().chain([from_at, to_at]) {
assert!(
buffer.cell(at).is_some(),
"{terminal:?} wrote nothing at {at:?}"
);
}
let mut counting = CountingSurface::default();
connector.draw(&mut counting);
assert_eq!(
counting.max_writes(),
1,
"{terminal:?} wrote some position more than once"
);
}
}
#[test]
fn facing_away_are_joined_at_any_distance() {
let near = render_connector(
Pos { x: 0, y: -1 },
Size {
width: 2,
height: 5,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Up),
endpoint(Pos { x: 0, y: 2 }, Direction::Down),
);
let far = render_connector(
Pos { x: 0, y: -1 },
Size {
width: 2,
height: 21,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Up),
endpoint(Pos { x: 0, y: 18 }, Direction::Down),
);
assert_eq!(near, "┌┐\n▼│\n │\n▲│\n└┘\n");
assert_eq!(far, format!("┌┐\n▼│\n{}▲│\n└┘\n", " │\n".repeat(17)));
}
#[test]
fn facing_away_off_column_takes_four_bends_around_the_outside() {
let text = render_connector(
Pos { x: -1, y: -1 },
Size {
width: 3,
height: 5,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Up),
endpoint(Pos { x: 1, y: 2 }, Direction::Down),
);
assert_eq!(text, "┌┐ \n│▼ \n│ \n│ ▲\n└─┘\n");
}
#[test]
fn the_former_double_escape_takes_four_bends_and_the_two_orders_mirror() {
let size = Size {
width: 5,
height: 3,
};
let leaving_left_first = render_connector(
Pos { x: -1, y: -1 },
size,
endpoint(Pos { x: 0, y: 0 }, Direction::Left),
endpoint(Pos { x: 2, y: 1 }, Direction::Right),
);
let leaving_right_first = render_connector(
Pos { x: -1, y: 0 },
size,
endpoint(Pos { x: 2, y: 1 }, Direction::Right),
endpoint(Pos { x: 0, y: 0 }, Direction::Left),
);
assert_eq!(leaving_left_first, "┌───┐\n└► │\n ◄┘\n");
assert_eq!(leaving_right_first, "┌► \n│ ◄┐\n└───┘\n");
let bends = |text: &str| text.chars().filter(|c| "┌┐└┘".contains(*c)).count();
assert_eq!(bends(&leaving_left_first), 4);
assert_eq!(bends(&leaving_right_first), 4);
}
#[test]
fn a_mirrored_route_passes_on_the_right_of_the_travel() {
let text = render_connector(
Pos { x: -1, y: 0 },
Size {
width: 2,
height: 4,
},
endpoint(Pos { x: 0, y: 0 }, Direction::Down),
endpoint(Pos { x: 0, y: 2 }, Direction::Down),
);
assert_eq!(text, " ▲\n┌┘\n│▲\n└┘\n");
}
#[test]
fn the_route_is_empty_only_where_the_model_says_it_is() {
let an_endpoint_stands_on_the_arrival_cell = derive_path(
Pos { x: 0, y: 0 },
Direction::Up,
Pos { x: 0, y: 1 },
Direction::Up,
);
let coincident_leaving_the_same_direction = derive_path(
Pos { x: 0, y: 0 },
Direction::Right,
Pos { x: 0, y: 0 },
Direction::Right,
);
assert_eq!(an_endpoint_stands_on_the_arrival_cell, None);
assert_eq!(coincident_leaving_the_same_direction, None);
}
#[test]
fn the_coincident_position_family_follows_the_rule() {
const DIRECTIONS: [Direction; 4] = [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
];
let at = Pos { x: 0, y: 0 };
let mut same_direction = 0;
let mut different_direction = 0;
for da in DIRECTIONS {
for db in DIRECTIONS {
let route = derive_path(at, da, at, db);
if da == db {
assert_eq!(route, None, "leaving {da:?} both ways draws no route");
same_direction += 1;
} else {
assert!(route.is_some(), "leaving {da:?} then {db:?} draws a route");
different_direction += 1;
}
}
}
assert_eq!(same_direction, 4);
assert_eq!(different_direction, 12);
}
#[test]
fn the_state_count_does_not_grow_with_distance() {
let states_apart = |cells_apart: i16| {
let s = Pos { x: 0, y: -1 };
let t = Pos {
x: 0,
y: cells_apart + 1,
};
let mid = RouteRectangle::spanning(s, t).middle(s);
let lattice = Lattice::new(s, t, mid);
lattice.x.len() * lattice.y.len() * 4
};
let four = states_apart(4);
let fifty = states_apart(50);
let five_hundred = states_apart(500);
assert_eq!(four, fifty);
assert_eq!(fifty, five_hundred);
assert!(four <= 196);
}
const SWEEP_ORIGIN: Pos = Pos { x: -2, y: -2 };
const SWEEP_SIZE: Size = Size {
width: 10,
height: 9,
};
type AnchorGroup = (Pos, Direction, Vec<(Pos, Direction)>);
fn sweep_arrangements_by_anchor() -> Vec<AnchorGroup> {
const DIRECTIONS: [Direction; 4] = [
Direction::Up,
Direction::Right,
Direction::Down,
Direction::Left,
];
let anchors = [Pos { x: 0, y: 0 }, Pos { x: 2, y: 1 }];
let mut groups = Vec::new();
for anchor in anchors {
for anchor_dir in DIRECTIONS {
let mut others = Vec::new();
for x in 0..6 {
for y in 0..5 {
let other = Pos { x, y };
if other == anchor {
continue;
}
for other_dir in DIRECTIONS {
others.push((other, other_dir));
}
}
}
groups.push((anchor, anchor_dir, others));
}
}
groups
}
fn window_contains(p: Pos) -> bool {
let dx =
p.x.checked_sub(SWEEP_ORIGIN.x)
.and_then(|d| u16::try_from(d).ok());
let dy =
p.y.checked_sub(SWEEP_ORIGIN.y)
.and_then(|d| u16::try_from(d).ok());
matches!((dx, dy), (Some(dx), Some(dy)) if dx < SWEEP_SIZE.width && dy < SWEEP_SIZE.height)
}
#[test]
fn sweep_every_route_fits_inside_the_window() {
for (a, da, b, db) in sweep_arrangements() {
for (from_at, from_dir, to_at, to_dir) in [(a, da, b, db), (b, db, a, da)] {
assert!(
window_contains(from_at) && window_contains(to_at),
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): a head outside the window"
);
if let Some(path) = derive_path(from_at, from_dir, to_at, to_dir) {
for pos in path {
assert!(
window_contains(pos),
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): route cell {pos:?} outside the window"
);
}
}
}
}
}
fn sweep_arrangements() -> Vec<(Pos, Direction, Pos, Direction)> {
sweep_arrangements_by_anchor()
.into_iter()
.flat_map(|(anchor, anchor_dir, others)| {
others
.into_iter()
.map(move |(other, other_dir)| (anchor, anchor_dir, other, other_dir))
})
.collect()
}
#[test]
fn the_sweep_grid_has_928_arrangements() {
assert_eq!(sweep_arrangements().len(), 928);
}
#[test]
fn sweep_every_endpoint_renders_its_own_head_and_no_position_is_written_twice() {
for (a, da, b, db) in sweep_arrangements() {
for (from_at, from_dir, to_at, to_dir) in [(a, da, b, db), (b, db, a, da)] {
let mut buffer = Buffer::new(Window {
origin: SWEEP_ORIGIN,
size: SWEEP_SIZE,
});
Connector {
from: endpoint(from_at, from_dir),
to: endpoint(to_at, to_dir),
stroke: light(),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
buffer.cell(from_at),
Some(&Cell::Literal(head_for(from_dir))),
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): from's own head"
);
assert_eq!(
buffer.cell(to_at),
Some(&Cell::Literal(head_for(to_dir))),
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}): to's own head"
);
let mut counting = CountingSurface::default();
Connector {
from: endpoint(from_at, from_dir),
to: endpoint(to_at, to_dir),
stroke: light(),
}
.draw(&mut counting);
assert!(
counting.max_writes() <= 1,
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?}) wrote a position more than once"
);
}
}
}
#[test]
fn sweep_matches_the_pinned_characterization() {
use std::fmt::Write as _;
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/snapshots/characterization"
));
settings.set_description(concat!(
"This file records what the code does over a range too wide to assert by hand. ",
"A change here is the consequence of a decision taken elsewhere, not a decision. ",
"It is accepted after a report of how many cases moved, in which families, and ",
"three examples with their before and after.",
));
settings.bind(|| {
for (anchor, anchor_dir, others) in sweep_arrangements_by_anchor() {
let mut rendered = String::new();
for (other, other_dir) in others {
for (from_at, from_dir, to_at, to_dir) in [
(anchor, anchor_dir, other, other_dir),
(other, other_dir, anchor, anchor_dir),
] {
let text = render_connector(
SWEEP_ORIGIN,
SWEEP_SIZE,
endpoint(from_at, from_dir),
endpoint(to_at, to_dir),
);
let trimmed: Vec<&str> = text.lines().map(str::trim_end).collect();
let _ = writeln!(
rendered,
"({from_at:?}, {from_dir:?}) -> ({to_at:?}, {to_dir:?})\n{}\n",
trimmed.join("\n")
);
}
}
let name = format!(
"connector_sweep_anchor_{}_{}_{anchor_dir:?}",
anchor.x, anchor.y
)
.to_lowercase();
insta::assert_snapshot!(name, rendered);
}
});
}
}