use std::cmp::Ordering;
use std::collections::BinaryHeap;
use unicode_width::UnicodeWidthChar;
mod rect {
pub const TL: char = '┌';
pub const TR: char = '┐';
pub const BL: char = '└';
pub const BR: char = '┘';
pub const H: char = '─';
pub const V: char = '│';
}
mod rounded {
pub const TL: char = '╭';
pub const TR: char = '╮';
pub const BL: char = '╰';
pub const BR: char = '╯';
}
pub mod arrow {
pub const RIGHT: char = '▸';
pub const DOWN: char = '▾';
pub const LEFT: char = '◂';
pub const UP: char = '▴';
}
pub mod endpoint {
pub const CIRCLE: char = '○';
pub const CROSS: char = '×';
}
mod dotted {
pub const H: char = '┄';
pub const V: char = '┆';
}
const THICK_DIR_TO_CHAR: [char; 16] = [
' ', '┃', '┃', '┃', '━', '┛', '┓', '┫', '━', '┗', '┏', '┣', '━', '┻', '┳', '╋', ];
const DIR_UP: u8 = 0b0001;
const DIR_DOWN: u8 = 0b0010;
const DIR_LEFT: u8 = 0b0100;
const DIR_RIGHT: u8 = 0b1000;
const DIR_TO_CHAR: [char; 16] = [
' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '─', '┴', '┬', '┼', ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Obstacle {
Free,
NodeBox,
EdgeOccupied,
}
#[derive(Debug, Clone, Copy)]
struct AstarNode {
f_cost: f32,
g_cost: f32,
col: usize,
row: usize,
dir: u8,
}
impl PartialEq for AstarNode {
fn eq(&self, other: &Self) -> bool {
self.f_cost == other.f_cost
}
}
impl Eq for AstarNode {}
impl Ord for AstarNode {
fn cmp(&self, other: &Self) -> Ordering {
other
.f_cost
.partial_cmp(&self.f_cost)
.unwrap_or(Ordering::Equal)
}
}
impl PartialOrd for AstarNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeLineStyle {
Solid,
Dotted,
Thick,
}
#[derive(Debug, Clone)]
pub struct Grid {
cells: Vec<Vec<char>>,
obstacles: Vec<Vec<Obstacle>>,
directions: Vec<Vec<u8>>,
protected: Vec<Vec<bool>>,
width: usize,
height: usize,
}
impl Grid {
pub fn new(width: usize, height: usize) -> Self {
Self {
cells: vec![vec![' '; width]; height],
obstacles: vec![vec![Obstacle::Free; width]; height],
directions: vec![vec![0u8; width]; height],
protected: vec![vec![false; width]; height],
width,
height,
}
}
fn add_dirs(&mut self, col: usize, row: usize, bits: u8) {
if row >= self.height || col >= self.width {
return;
}
if self.protected[row][col] {
return;
}
self.directions[row][col] |= bits;
self.cells[row][col] = DIR_TO_CHAR[self.directions[row][col] as usize];
}
fn protect(&mut self, col: usize, row: usize) {
if row < self.height && col < self.width {
self.protected[row][col] = true;
}
}
pub fn mark_node_box(&mut self, col: usize, row: usize, w: usize, h: usize) {
for dy in 0..h {
for dx in 0..w {
let r = row + dy;
let c = col + dx;
if r < self.height && c < self.width {
self.obstacles[r][c] = Obstacle::NodeBox;
}
}
}
}
pub fn mark_obstacle(&mut self, col: usize, row: usize) {
if row < self.height && col < self.width {
self.obstacles[row][col] = Obstacle::NodeBox;
}
}
pub fn protect_cell(&mut self, col: usize, row: usize) {
self.protect(col, row);
}
pub fn unprotect_cell(&mut self, col: usize, row: usize) {
if row < self.height && col < self.width {
self.protected[row][col] = false;
}
}
pub fn recompute_cell_glyph(&mut self, col: usize, row: usize) {
if row < self.height && col < self.width {
let bits = self.directions[row][col];
self.cells[row][col] = DIR_TO_CHAR[bits as usize];
}
}
pub fn set(&mut self, col: usize, row: usize, ch: char) {
if row < self.height && col < self.width {
self.cells[row][col] = ch;
}
}
pub fn get(&self, col: usize, row: usize) -> char {
if row < self.height && col < self.width {
self.cells[row][col]
} else {
' '
}
}
pub fn render(&self) -> String {
self.to_string()
}
pub fn draw_box(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.set(col, row, rect::TL);
self.set(col + w - 1, row, rect::TR);
self.set(col, row + h - 1, rect::BL);
self.set(col + w - 1, row + h - 1, rect::BR);
for x in (col + 1)..(col + w - 1) {
self.set(x, row, rect::H);
self.set(x, row + h - 1, rect::H);
}
for y in (row + 1)..(row + h - 1) {
self.set(col, y, rect::V);
self.set(col + w - 1, y, rect::V);
}
}
pub fn draw_rounded_box(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.set(col, row, rounded::TL);
self.set(col + w - 1, row, rounded::TR);
self.set(col, row + h - 1, rounded::BL);
self.set(col + w - 1, row + h - 1, rounded::BR);
for x in (col + 1)..(col + w - 1) {
self.set(x, row, rect::H);
self.set(x, row + h - 1, rect::H);
}
for y in (row + 1)..(row + h - 1) {
self.set(col, y, rect::V);
self.set(col + w - 1, y, rect::V);
}
}
pub fn draw_diamond(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
let cx = col + w / 2;
self.set(cx, row, '◇');
self.set(cx, row + h - 1, '◇');
}
pub fn draw_stadium(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 4 || h < 2 {
return;
}
self.draw_rounded_box(col, row, w, h);
let mid_row = row + h / 2;
self.set(col + 1, mid_row, '(');
self.set(col + w - 2, mid_row, ')');
self.protect(col + 1, mid_row);
self.protect(col + w - 2, mid_row);
}
pub fn draw_subroutine(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 4 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
for y in (row + 1)..(row + h - 1) {
self.set(col + 1, y, rect::V);
self.set(col + w - 2, y, rect::V);
}
}
pub fn draw_cylinder(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 4 {
return;
}
self.set(col, row, rounded::TL);
self.set(col + w - 1, row, rounded::TR);
for x in (col + 1)..(col + w - 1) {
self.set(x, row, rect::H);
}
self.set(col, row + 1, '├');
self.set(col + w - 1, row + 1, '┤');
for x in (col + 1)..(col + w - 1) {
self.set(x, row + 1, rect::H);
}
for y in (row + 2)..(row + h - 1) {
self.set(col, y, rect::V);
self.set(col + w - 1, y, rect::V);
}
self.set(col, row + h - 1, rounded::BL);
self.set(col + w - 1, row + h - 1, rounded::BR);
for x in (col + 1)..(col + w - 1) {
self.set(x, row + h - 1, rect::H);
}
}
pub fn draw_hexagon(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 4 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
let mid_row = row + h / 2;
self.set(col, mid_row, '<');
self.set(col + w - 1, mid_row, '>');
self.protect(col, mid_row);
self.protect(col + w - 1, mid_row);
}
pub fn draw_asymmetric(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
let mid_row = row + h / 2;
self.set(col + w - 1, mid_row, '⟩');
self.protect(col + w - 1, mid_row);
}
pub fn draw_parallelogram(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
self.set(col, row, '/');
self.set(col + w - 1, row + h - 1, '/');
self.protect(col, row);
self.protect(col + w - 1, row + h - 1);
}
pub fn draw_trapezoid(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 2 || h < 2 {
return;
}
self.draw_box(col, row, w, h);
self.set(col, row, '/');
self.set(col + w - 1, row, '\\');
self.protect(col, row);
self.protect(col + w - 1, row);
}
pub fn draw_double_circle(&mut self, col: usize, row: usize, w: usize, h: usize) {
if w < 5 || h < 5 {
return;
}
self.draw_rounded_box(col, row, w, h);
self.draw_rounded_box(col + 1, row + 1, w - 2, h - 2);
}
pub fn write_text(&mut self, col: usize, row: usize, text: &str) {
let mut x = col;
for ch in text.chars() {
if x >= self.width {
break;
}
self.set(x, row, ch);
x += UnicodeWidthChar::width(ch).unwrap_or(1);
}
}
pub fn write_text_protected(&mut self, col: usize, row: usize, text: &str) {
let mut x = col;
for ch in text.chars() {
if x >= self.width {
break;
}
self.set(x, row, ch);
self.protect(x, row);
x += UnicodeWidthChar::width(ch).unwrap_or(1);
}
}
pub fn draw_h_arrow(&mut self, col1: usize, row: usize, col2: usize) {
if col1 >= col2 {
return;
}
for x in col1..col2 {
self.add_dirs(x, row, DIR_LEFT | DIR_RIGHT);
}
self.set(col2, row, arrow::RIGHT);
self.protect(col2, row);
}
pub fn draw_v_arrow(&mut self, col: usize, row1: usize, row2: usize) {
if row1 >= row2 {
return;
}
for y in row1..row2 {
self.add_dirs(col, y, DIR_UP | DIR_DOWN);
}
self.set(col, row2, arrow::DOWN);
self.protect(col, row2);
}
pub fn draw_manhattan(
&mut self,
col1: usize,
row1: usize,
col2: usize,
row2: usize,
horizontal_first: bool,
arrow_direction: char,
) {
if col1 == col2 && row1 == row2 {
return;
}
if horizontal_first {
if col1 != col2 {
let (lo, hi) = order(col1, col2);
for x in lo..hi {
self.add_dirs(x, row1, DIR_LEFT | DIR_RIGHT);
}
}
if row1 == row2 {
self.set(col2, row2, arrow_direction);
self.protect(col2, row2);
} else {
let h_in = if col2 > col1 { DIR_LEFT } else { DIR_RIGHT };
let v_out = if row2 > row1 { DIR_DOWN } else { DIR_UP };
self.add_dirs(col2, row1, h_in | v_out);
let (vlo, vhi) = order(row1, row2);
for y in (vlo + 1)..vhi {
self.add_dirs(col2, y, DIR_UP | DIR_DOWN);
}
self.set(col2, row2, arrow_direction);
self.protect(col2, row2);
}
} else {
if row1 != row2 {
let (lo, hi) = order(row1, row2);
for y in lo..hi {
self.add_dirs(col1, y, DIR_UP | DIR_DOWN);
}
}
if col1 == col2 {
self.set(col2, row2, arrow_direction);
self.protect(col2, row2);
} else {
let v_in = if row2 > row1 { DIR_UP } else { DIR_DOWN };
let h_out = if col2 > col1 { DIR_RIGHT } else { DIR_LEFT };
self.add_dirs(col1, row2, v_in | h_out);
let (hlo, hhi) = order(col1, col2);
for x in (hlo + 1)..hhi {
self.add_dirs(x, row2, DIR_LEFT | DIR_RIGHT);
}
self.set(col2, row2, arrow_direction);
self.protect(col2, row2);
}
}
}
pub fn route_edge(
&mut self,
col1: usize,
row1: usize,
col2: usize,
row2: usize,
horizontal_first: bool,
arrow_direction: char,
) -> Option<Vec<(usize, usize)>> {
const EDGE_SOFT_COST: f32 = 4.0;
const CORNER_PENALTY: f32 = 0.5;
const DIRS: [(isize, isize); 4] = [(1, 0), (0, 1), (-1, 0), (0, -1)];
if col1 == col2 && row1 == row2 {
return None;
}
let h = |c: usize, r: usize| -> f32 { (c.abs_diff(col2) + r.abs_diff(row2)) as f32 };
let mut g_cost: Vec<Vec<f32>> = vec![vec![f32::INFINITY; self.width]; self.height];
let mut came_from: Vec<Vec<u8>> = vec![vec![u8::MAX; self.width]; self.height];
g_cost[row1][col1] = 0.0;
let mut open: BinaryHeap<AstarNode> = BinaryHeap::new();
let start_dir = if horizontal_first { 0u8 } else { 1u8 };
open.push(AstarNode {
f_cost: h(col1, row1),
g_cost: 0.0,
col: col1,
row: row1,
dir: start_dir,
});
'outer: while let Some(current) = open.pop() {
if current.g_cost > g_cost[current.row][current.col] {
continue;
}
if current.col == col2 && current.row == row2 {
break 'outer;
}
for (dir_idx, &(dc, dr)) in DIRS.iter().enumerate() {
let nc = current.col.wrapping_add_signed(dc);
let nr = current.row.wrapping_add_signed(dr);
if nc >= self.width || nr >= self.height {
continue;
}
if self.obstacles[nr][nc] == Obstacle::NodeBox {
if nc != col2 || nr != row2 {
continue;
}
}
let mut step = 1.0f32;
if self.obstacles[nr][nc] == Obstacle::EdgeOccupied {
step += EDGE_SOFT_COST;
}
if dir_idx as u8 != current.dir {
step += CORNER_PENALTY;
}
let new_g = current.g_cost + step;
if new_g < g_cost[nr][nc] {
g_cost[nr][nc] = new_g;
came_from[nr][nc] = dir_idx as u8;
open.push(AstarNode {
f_cost: new_g + h(nc, nr),
g_cost: new_g,
col: nc,
row: nr,
dir: dir_idx as u8,
});
}
}
}
if came_from[row2][col2] == u8::MAX && (col1 != col2 || row1 != row2) {
self.draw_manhattan(col1, row1, col2, row2, horizontal_first, arrow_direction);
return Some(vec![(col1, row1), (col2, row2)]);
}
let mut path: Vec<(usize, usize)> = Vec::new();
let mut cc = col2;
let mut cr = row2;
path.push((cc, cr));
while cc != col1 || cr != row1 {
let dir = came_from[cr][cc];
if dir == u8::MAX {
break;
}
let (dc, dr) = DIRS[dir as usize];
cc = cc.wrapping_add_signed(-dc);
cr = cr.wrapping_add_signed(-dr);
path.push((cc, cr));
}
path.reverse();
self.draw_routed_path(&path, arrow_direction);
Some(path)
}
pub fn overdraw_path_style(&mut self, path_cells: &[(usize, usize)], style: EdgeLineStyle) {
if style == EdgeLineStyle::Solid {
return;
}
for &(c, r) in path_cells {
if r >= self.height || c >= self.width {
continue;
}
if self.protected[r][c] {
continue;
}
let bits = self.directions[r][c];
match style {
EdgeLineStyle::Solid => {}
EdgeLineStyle::Dotted => {
self.cells[r][c] = match bits {
0b0001..=0b0011 => dotted::V, 0b0100 | 0b1000 | 0b1100 => dotted::H, _ => DIR_TO_CHAR[bits as usize], };
}
EdgeLineStyle::Thick => {
self.cells[r][c] = THICK_DIR_TO_CHAR[bits as usize];
}
}
}
}
fn draw_routed_path(&mut self, path: &[(usize, usize)], tip: char) {
if path.len() < 2 {
return;
}
let last = path.len() - 1;
for i in 0..=last {
let (c, r) = path[i];
if r < self.height && c < self.width && self.obstacles[r][c] != Obstacle::NodeBox {
self.obstacles[r][c] = Obstacle::EdgeOccupied;
}
if i == last {
self.set(c, r, tip);
self.protect(c, r);
continue;
}
let mut bits = 0u8;
if i > 0 {
let (pc, pr) = path[i - 1];
bits |= neighbor_bit(c, r, pc, pr);
}
let (nc, nr) = path[i + 1];
bits |= neighbor_bit(c, r, nc, nr);
self.add_dirs(c, r, bits);
}
}
}
impl std::fmt::Display for Grid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = String::with_capacity(self.height * (self.width + 1));
for row in &self.cells {
let line: String = row.iter().collect();
out.push_str(line.trim_end());
out.push('\n');
}
while out.ends_with("\n\n") {
out.pop();
}
write!(f, "{out}")
}
}
fn order(a: usize, b: usize) -> (usize, usize) {
if a <= b { (a, b) } else { (b, a) }
}
fn neighbor_bit(c: usize, r: usize, nc: usize, nr: usize) -> u8 {
if nc < c {
DIR_LEFT
} else if nc > c {
DIR_RIGHT
} else if nr < r {
DIR_UP
} else if nr > r {
DIR_DOWN
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_set_and_get() {
let mut g = Grid::new(5, 5);
g.set(2, 3, 'X');
assert_eq!(g.get(2, 3), 'X');
assert_eq!(g.get(0, 0), ' ');
}
#[test]
fn out_of_bounds_ignored() {
let mut g = Grid::new(3, 3);
g.set(10, 10, 'X'); assert_eq!(g.get(10, 10), ' ');
}
#[test]
fn draw_box_corners() {
let mut g = Grid::new(10, 5);
g.draw_box(0, 0, 5, 3);
assert_eq!(g.get(0, 0), '┌');
assert_eq!(g.get(4, 0), '┐');
assert_eq!(g.get(0, 2), '└');
assert_eq!(g.get(4, 2), '┘');
}
#[test]
fn write_text_respects_width() {
let mut g = Grid::new(20, 3);
g.write_text(1, 1, "Hello");
assert_eq!(g.get(1, 1), 'H');
assert_eq!(g.get(5, 1), 'o');
}
#[test]
fn to_string_strips_trailing_spaces() {
let g = Grid::new(10, 2);
let s = g.to_string();
for line in s.lines() {
assert!(!line.ends_with(' '));
}
}
#[test]
fn draw_h_arrow_places_tip() {
let mut g = Grid::new(20, 3);
g.draw_h_arrow(2, 1, 8);
assert_eq!(g.get(8, 1), arrow::RIGHT);
assert_eq!(g.get(2, 1), '─');
}
#[test]
fn draw_v_arrow_places_tip() {
let mut g = Grid::new(10, 10);
g.draw_v_arrow(3, 1, 5);
assert_eq!(g.get(3, 5), arrow::DOWN);
assert_eq!(g.get(3, 1), '│');
}
}