use std::cmp::Ordering;
use std::collections::BinaryHeap;
use unicode_width::UnicodeWidthChar;
use crate::types::Rgb;
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 = '┆';
}
pub const BAR_THICKNESS: usize = 3;
const THICK_DIR_TO_CHAR: [char; 16] = [
' ', '┃', '┃', '┃', '━', '┛', '┓', '┫', '━', '┗', '┏', '┣', '━', '┻', '┳', '╋', ];
pub(crate) const DIR_UP: u8 = 0b0001;
pub(crate) const DIR_DOWN: u8 = 0b0010;
pub(crate) const DIR_LEFT: u8 = 0b0100;
pub(crate) const DIR_RIGHT: u8 = 0b1000;
const DIR_TO_CHAR: [char; 16] = [
' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '─', '┴', '┬', '┼', ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Obstacle {
Free,
NodeBox,
EdgeOccupiedHorizontal,
EdgeOccupiedVertical,
InnerArea,
}
#[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, Copy, PartialEq, Eq)]
pub(crate) struct Attach {
pub col: usize,
pub row: usize,
}
#[derive(Debug, Clone)]
pub struct Grid {
cells: Vec<Vec<char>>,
obstacles: Vec<Vec<Obstacle>>,
directions: Vec<Vec<u8>>,
protected: Vec<Vec<bool>>,
fg: Vec<Vec<Option<Rgb>>>,
bg: Vec<Vec<Option<Rgb>>>,
hyperlink: Vec<Vec<Option<u32>>>,
hyperlink_urls: Vec<String>,
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],
fg: vec![vec![None; width]; height],
bg: vec![vec![None; width]; height],
hyperlink: vec![vec![None; width]; height],
hyperlink_urls: Vec::new(),
width,
height,
}
}
pub(crate) fn rows(&self) -> usize {
self.height
}
pub(crate) fn add_dirs(&mut self, col: usize, row: usize, bits: u8) {
if row >= self.height || col >= self.width {
return;
}
if self.protected[row][col] && self.directions[row][col] == 0 {
return;
}
self.directions[row][col] |= bits;
self.cells[row][col] = DIR_TO_CHAR[self.directions[row][col] as usize];
}
pub fn seed_border_dirs(&mut self, col: usize, row: usize, bits: u8) {
if row < self.height && col < self.width {
self.directions[row][col] |= bits;
}
}
pub(crate) fn clear_dirs(&mut self, col: usize, row: usize) {
if row < self.height && col < self.width {
self.directions[row][col] = 0;
}
}
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 mark_inner_area(&mut self, col: usize, row: usize, w: usize, h: usize) {
for dy in 0..h {
let r = row + dy;
if r >= self.height {
break;
}
for dx in 0..w {
let c = col + dx;
if c >= self.width {
break;
}
if self.obstacles[r][c] == Obstacle::Free {
self.obstacles[r][c] = Obstacle::InnerArea;
}
}
}
}
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 set_unless_protected(&mut self, col: usize, row: usize, ch: char) {
if row < self.height && col < self.width && !self.protected[row][col] {
self.cells[row][col] = ch;
}
}
pub(crate) fn is_node_box(&self, col: usize, row: usize) -> bool {
if row >= self.height || col >= self.width {
return true; }
self.obstacles[row][col] == Obstacle::NodeBox
}
pub(crate) fn edge_occupied_cost(&self, col: usize, row: usize) -> u32 {
if row >= self.height || col >= self.width {
return u32::MAX / 2;
}
match self.obstacles[row][col] {
Obstacle::Free | Obstacle::InnerArea => 0,
Obstacle::EdgeOccupiedHorizontal | Obstacle::EdgeOccupiedVertical => 1,
Obstacle::NodeBox => u32::MAX / 2,
}
}
pub fn get(&self, col: usize, row: usize) -> char {
if row < self.height && col < self.width {
self.cells[row][col]
} else {
' '
}
}
pub fn set_fg(&mut self, col: usize, row: usize, c: Rgb) {
if row < self.height && col < self.width {
self.fg[row][col] = Some(c);
}
}
pub fn set_bg(&mut self, col: usize, row: usize, c: Rgb) {
if row < self.height && col < self.width {
self.bg[row][col] = Some(c);
}
}
pub fn paint_fg_rect(&mut self, col: usize, row: usize, w: usize, h: usize, c: Rgb) {
for dy in 0..h {
for dx in 0..w {
self.set_fg(col + dx, row + dy, c);
}
}
}
pub fn paint_bg_rect(&mut self, col: usize, row: usize, w: usize, h: usize, c: Rgb) {
for dy in 0..h {
for dx in 0..w {
self.set_bg(col + dx, row + dy, c);
}
}
}
pub fn paint_fg_path(&mut self, path: &[(usize, usize)], c: Rgb) {
for &(col, row) in path {
self.set_fg(col, row, c);
}
}
pub fn paint_hyperlink(&mut self, col: usize, row: usize, w: usize, h: usize, url: &str) {
let idx = self
.hyperlink_urls
.iter()
.position(|u| u == url)
.map(|i| i as u32)
.unwrap_or_else(|| {
let i = self.hyperlink_urls.len() as u32;
self.hyperlink_urls.push(url.to_string());
i
});
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.hyperlink[r][c] = Some(idx);
}
}
}
}
pub fn render_with_colors(&self) -> String {
self.render_inner(true)
}
pub fn render(&self) -> String {
if self.hyperlink_urls.is_empty() {
self.to_string()
} else {
self.render_inner(false)
}
}
fn render_inner(&self, with_color: bool) -> String {
use std::fmt::Write as FmtWrite;
let has_hyperlinks = !self.hyperlink_urls.is_empty();
let mut out = String::with_capacity(self.height * (self.width + 32));
let mut row_buf = String::with_capacity(self.width + 64);
for row in 0..self.height {
row_buf.clear();
let mut current_fg: Option<Rgb> = None;
let mut current_bg: Option<Rgb> = None;
let mut any_sgr_in_row = false;
let mut current_hl: Option<u32> = None;
for col in 0..self.width {
if has_hyperlinks {
let cell_hl = self.hyperlink[row][col];
if cell_hl != current_hl {
if current_hl.is_some() {
row_buf.push_str("\x1b]8;;\x1b\\");
}
if let Some(idx) = cell_hl {
let url = &self.hyperlink_urls[idx as usize];
let _ = write!(row_buf, "\x1b]8;;{url}\x1b\\");
}
current_hl = cell_hl;
}
}
if with_color {
let fg = self.fg[row][col];
let bg = self.bg[row][col];
if fg != current_fg || bg != current_bg {
if any_sgr_in_row {
row_buf.push_str("\x1b[0m");
}
if let Some(Rgb(r, g, b)) = fg {
let _ = write!(row_buf, "\x1b[38;2;{r};{g};{b}m");
any_sgr_in_row = true;
}
if let Some(Rgb(r, g, b)) = bg {
let _ = write!(row_buf, "\x1b[48;2;{r};{g};{b}m");
any_sgr_in_row = true;
}
current_fg = fg;
current_bg = bg;
}
}
row_buf.push(self.cells[row][col]);
}
if has_hyperlinks && current_hl.is_some() {
row_buf.push_str("\x1b]8;;\x1b\\");
}
while row_buf.ends_with(' ') {
row_buf.pop();
}
if with_color && any_sgr_in_row {
row_buf.push_str("\x1b[0m");
}
out.push_str(&row_buf);
out.push('\n');
}
while out.ends_with("\n\n") {
out.pop();
}
while out.starts_with('\n') {
out.remove(0);
}
out
}
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_unless_protected(x, row, rect::H);
self.set_unless_protected(x, row + h - 1, rect::H);
}
for y in (row + 1)..(row + h - 1) {
self.set_unless_protected(col, y, rect::V);
self.set_unless_protected(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_unless_protected(x, row, rect::H);
self.set_unless_protected(x, row + h - 1, rect::H);
}
for y in (row + 1)..(row + h - 1) {
self.set_unless_protected(col, y, rect::V);
self.set_unless_protected(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);
self.set(col, row, '╱');
self.set(col + w - 1, row, '╲');
self.set(col, row + h - 1, '╲');
self.set(col + w - 1, row + h - 1, '╱');
}
fn fill_block(&mut self, col: usize, row: usize, w: usize, h: usize) {
for y in row..(row + h) {
for x in col..(col + w) {
self.set(x, y, '█');
}
}
}
pub fn draw_horizontal_bar(&mut self, col: usize, row: usize, w: usize) {
self.fill_block(col, row, w, BAR_THICKNESS);
}
pub fn draw_vertical_bar(&mut self, col: usize, row: usize, h: usize) {
self.fill_block(col, row, BAR_THICKNESS, h);
}
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, mid_row, '(');
self.set(col + w - 1, mid_row, ')');
self.protect(col, mid_row);
self.protect(col + w - 1, 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 < 4 || h < 4 {
return;
}
self.draw_rounded_box(col, row, w, h);
for x in (col + 2)..(col + w - 2) {
self.set(x, row + 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);
self.set(col, row, '╱');
self.set(col + w - 1, row, '╲');
self.set(col, row + h - 1, '╲');
self.set(col + w - 1, row + h - 1, '╱');
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, '╱');
self.set(col, row + h - 1, '╱');
self.set(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_parallelogram_backslash(&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.set(col, row + h - 1, '╲');
self.set(col + w - 1, row + h - 1, '╲');
}
pub fn draw_trapezoid_inverted(&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)>> {
self.route_edge_with_inner_cost(
col1,
row1,
col2,
row2,
horizontal_first,
arrow_direction,
0.0,
)
}
pub fn route_back_edge(
&mut self,
col1: usize,
row1: usize,
col2: usize,
row2: usize,
horizontal_first: bool,
arrow_direction: char,
) -> Option<Vec<(usize, usize)>> {
const BACK_EDGE_INNER_COST: f32 = 8.0;
self.route_edge_with_inner_cost(
col1,
row1,
col2,
row2,
horizontal_first,
arrow_direction,
BACK_EDGE_INNER_COST,
)
}
#[allow(clippy::too_many_arguments)]
fn route_edge_with_inner_cost(
&mut self,
col1: usize,
row1: usize,
col2: usize,
row2: usize,
horizontal_first: bool,
arrow_direction: char,
inner_area_cost: f32,
) -> Option<Vec<(usize, usize)>> {
const SAME_AXIS_COST: f32 = 10.0;
const CROSS_AXIS_COST: f32 = 3.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;
let moving_horizontal = dir_idx == 0 || dir_idx == 2;
match self.obstacles[nr][nc] {
Obstacle::EdgeOccupiedHorizontal => {
step += if moving_horizontal {
SAME_AXIS_COST } else {
CROSS_AXIS_COST };
}
Obstacle::EdgeOccupiedVertical => {
step += if moving_horizontal {
CROSS_AXIS_COST } else {
SAME_AXIS_COST };
}
_ => {}
}
if self.obstacles[nr][nc] == Obstacle::InnerArea {
step += inner_area_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 {
let obstacle = if i < last {
let (nc, nr) = path[i + 1];
if nc != c {
Obstacle::EdgeOccupiedHorizontal
} else if nr != r {
Obstacle::EdgeOccupiedVertical
} else {
self.obstacles[r][c]
}
} else {
let (pc, _) = path[i - 1];
if pc != c {
Obstacle::EdgeOccupiedHorizontal
} else {
Obstacle::EdgeOccupiedVertical
}
};
if !matches!(
self.obstacles[r][c],
Obstacle::EdgeOccupiedHorizontal | Obstacle::EdgeOccupiedVertical
) {
self.obstacles[r][c] = obstacle;
}
}
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);
}
}
#[allow(dead_code)] pub(crate) fn erase_path(&mut self, path: &[(usize, usize)]) {
if path.len() < 2 {
return;
}
let last = path.len() - 1;
for i in 0..=last {
let (c, r) = path[i];
if c >= self.width || r >= self.height {
continue;
}
if i == last {
self.protected[r][c] = false;
if self.directions[r][c] == 0 {
self.cells[r][c] = ' ';
} else {
self.cells[r][c] = DIR_TO_CHAR[self.directions[r][c] as usize];
}
continue;
}
let mut our_bits = 0u8;
if i > 0 {
let (pc, pr) = path[i - 1];
our_bits |= neighbor_bit(c, r, pc, pr);
}
let (nc, nr) = path[i + 1];
our_bits |= neighbor_bit(c, r, nc, nr);
if our_bits != 0 && self.directions[r][c] & our_bits == our_bits {
self.directions[r][c] &= !our_bits;
self.cells[r][c] = DIR_TO_CHAR[self.directions[r][c] as usize];
}
}
}
pub(crate) fn draw_path(
&mut self,
path: Vec<(usize, usize)>,
tip: char,
) -> Option<Vec<(usize, usize)>> {
if path.len() < 2 {
return None;
}
self.draw_routed_path(&path, tip);
Some(path)
}
}
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();
}
while out.starts_with('\n') {
out.remove(0);
}
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 erase_path_clears_isolated_segment() {
let mut g = Grid::new(10, 5);
let path = vec![(2, 2), (3, 2), (4, 2), (5, 2)];
g.draw_routed_path(&path, '▶');
assert_eq!(g.get(3, 2), '─');
assert_eq!(g.get(5, 2), '▶');
g.erase_path(&path);
for (c, r) in &path {
assert_eq!(
g.get(*c, *r),
' ',
"cell ({c},{r}) not cleared after erase"
);
}
g.add_dirs(5, 2, DIR_LEFT | DIR_RIGHT);
assert_eq!(g.get(5, 2), '─');
}
#[test]
fn erase_path_preserves_shared_junction() {
let mut g = Grid::new(10, 5);
let h_path = vec![(1, 2), (2, 2), (3, 2), (4, 2)];
g.draw_routed_path(&h_path, '▶');
let v_path = vec![(2, 0), (2, 1), (2, 2), (2, 3)];
g.draw_routed_path(&v_path, '▼');
assert_eq!(g.get(2, 2), '┼');
g.erase_path(&h_path);
assert_eq!(g.get(2, 2), '│', "junction collapsed to vertical bit only");
assert_eq!(g.get(1, 2), ' ');
assert_eq!(g.get(3, 2), ' ');
assert_eq!(g.get(2, 1), '│');
}
#[test]
fn erase_path_handles_tip_unprotect() {
let mut g = Grid::new(10, 5);
let path = vec![(2, 2), (3, 2), (4, 2)];
g.draw_routed_path(&path, '▶');
assert_eq!(g.get(4, 2), '▶');
g.erase_path(&path);
assert_eq!(g.get(4, 2), ' ');
g.add_dirs(4, 2, DIR_LEFT | DIR_RIGHT);
assert_eq!(g.get(4, 2), '─');
}
#[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), '│');
}
}