#[allow(unused_imports)] use crate::primitives::{ArrowType, BoxStyle};
use crate::{grid::Grid, primitives::PrimitiveInventory};
#[allow(dead_code)] pub fn render_diagram(inventory: &PrimitiveInventory) -> Grid {
let (max_row, max_col) = calculate_bounds(inventory);
let grid_lines: Vec<String> = (0..=max_row).map(|_| " ".repeat(max_col + 1)).collect();
let grid_strs: Vec<&str> = grid_lines.iter().map(String::as_str).collect();
let mut grid = Grid::from_lines(&grid_strs);
for b in &inventory.boxes {
draw_box(&mut grid, b);
}
for row in &inventory.text_rows {
if !row.content.trim().is_empty() {
draw_text_row(&mut grid, row);
}
}
for arrow in &inventory.horizontal_arrows {
if !is_position_inside_any_box(&inventory.boxes, arrow.row, arrow.start_col)
&& !is_position_inside_any_box(&inventory.boxes, arrow.row, arrow.end_col)
{
draw_horizontal_arrow(&mut grid, arrow);
}
}
for arrow in &inventory.vertical_arrows {
if !is_position_inside_any_box(&inventory.boxes, arrow.start_row, arrow.col)
&& !is_position_inside_any_box(&inventory.boxes, arrow.end_row, arrow.col)
{
draw_vertical_arrow(&mut grid, arrow);
}
}
for conn in &inventory.connection_lines {
draw_connection_line(&mut grid, conn);
}
for label in &inventory.labels {
draw_label(&mut grid, label);
}
grid
}
fn is_position_inside_any_box(boxes: &[crate::primitives::Box], row: usize, col: usize) -> bool {
boxes.iter().any(|b| {
row > b.top_left.0 && row < b.bottom_right.0 && col > b.top_left.1 && col < b.bottom_right.1
})
}
#[must_use]
#[allow(dead_code)] pub fn render_onto_grid(
original: &Grid,
original_inventory: &PrimitiveInventory,
inventory: &PrimitiveInventory,
) -> Grid {
let (max_row, max_col) = calculate_bounds(inventory);
let original_height = original.height();
let original_width = original.width();
let required_height = max_row.max(original_height.saturating_sub(1)) + 1;
let required_width = max_col.max(original_width.saturating_sub(1)) + 1;
let mut grid = if required_height > original_height || required_width > original_width {
let mut new_rows: Vec<Vec<char>> = Vec::with_capacity(required_height);
for row_idx in 0..required_height {
if row_idx < original_height {
let mut row: Vec<char> = Vec::with_capacity(required_width);
for col_idx in 0..required_width {
if col_idx < original_width {
let ch = original.get(row_idx, col_idx).unwrap_or(' ');
if matches!(
ch,
'↓' | '↑' | '→' | '←' | '⇓' | '⇑' | '⇒' | '⇐' | '⟶' | '⟹'
) {
row.push(' ');
} else {
row.push(ch);
}
} else {
row.push(' ');
}
}
new_rows.push(row);
} else {
new_rows.push(vec![' '; required_width]);
}
}
Grid::from_rows(new_rows)
} else {
let grid = original.clone();
let mut new_rows = Vec::new();
for row_idx in 0..grid.height() {
let mut row = Vec::new();
for col_idx in 0..required_width {
let ch = grid.get(row_idx, col_idx).unwrap_or(' ');
if matches!(
ch,
'↓' | '↑' | '→' | '←' | '⇓' | '⇑' | '⇒' | '⇐' | '⟶' | '⟹'
) {
row.push(' ');
} else {
row.push(ch);
}
}
new_rows.push(row);
}
Grid::from_rows(new_rows)
};
for b in &original_inventory.boxes {
clear_box_borders(&mut grid, b);
}
for b in &inventory.boxes {
draw_box(&mut grid, b);
}
for row in &inventory.text_rows {
if !row.content.trim().is_empty() {
draw_text_row(&mut grid, row);
}
}
for arrow in &inventory.horizontal_arrows {
if !is_position_inside_any_box(&inventory.boxes, arrow.row, arrow.start_col)
&& !is_position_inside_any_box(&inventory.boxes, arrow.row, arrow.end_col)
{
draw_horizontal_arrow(&mut grid, arrow);
}
}
for arrow in &inventory.vertical_arrows {
if !is_position_inside_any_box(&inventory.boxes, arrow.start_row, arrow.col)
&& !is_position_inside_any_box(&inventory.boxes, arrow.end_row, arrow.col)
{
draw_vertical_arrow(&mut grid, arrow);
}
}
for conn in &inventory.connection_lines {
draw_connection_line(&mut grid, conn);
}
for label in &inventory.labels {
draw_label(&mut grid, label);
}
grid
}
fn calculate_bounds(inventory: &PrimitiveInventory) -> (usize, usize) {
let mut max_row = 0;
let mut max_col = 0;
for b in &inventory.boxes {
max_row = max_row.max(b.bottom_right.0);
max_col = max_col.max(b.bottom_right.1);
}
for row in &inventory.text_rows {
max_row = max_row.max(row.row);
max_col = max_col.max(row.end_col);
}
for arrow in &inventory.horizontal_arrows {
max_row = max_row.max(arrow.row);
max_col = max_col.max(arrow.end_col);
}
for arrow in &inventory.vertical_arrows {
max_row = max_row.max(arrow.end_row);
max_col = max_col.max(arrow.col);
}
for conn in &inventory.connection_lines {
for segment in &conn.segments {
match segment {
crate::primitives::Segment::Horizontal {
row,
start_col: _,
end_col,
} => {
max_row = max_row.max(*row);
max_col = max_col.max(*end_col);
}
crate::primitives::Segment::Vertical {
col,
start_row: _,
end_row,
} => {
max_row = max_row.max(*end_row);
max_col = max_col.max(*col);
}
}
}
}
for label in &inventory.labels {
let label_end_col = label.col + label.content.len().saturating_sub(1);
max_row = max_row.max(label.row);
max_col = max_col.max(label_end_col);
}
(max_row, max_col)
}
fn clear_box_borders(grid: &mut Grid, b: &crate::primitives::Box) {
for col in b.top_left.1..=b.bottom_right.1 {
if let Some(cell) = grid.get_mut(b.top_left.0, col) {
if !is_junction_char(*cell) {
*cell = ' ';
}
}
if let Some(cell) = grid.get_mut(b.bottom_right.0, col) {
if !is_junction_char(*cell) {
*cell = ' ';
}
}
}
for row in (b.top_left.0 + 1)..b.bottom_right.0 {
if let Some(cell) = grid.get_mut(row, b.top_left.1) {
if !is_junction_char(*cell) {
*cell = ' ';
}
}
for col in [
b.bottom_right.1.saturating_sub(1),
b.bottom_right.1,
b.bottom_right.1 + 1,
] {
if let Some(cell) = grid.get_mut(row, col) {
if matches!(*cell, '│' | '║' | '┃') {
*cell = ' ';
}
}
}
}
}
const fn is_junction_char(ch: char) -> bool {
matches!(
ch,
'┬' | '┴' | '├' | '┤' | '┼' | '╦' | '╩' | '╠' | '╣' | '╬'
)
}
fn draw_box(grid: &mut Grid, b: &crate::primitives::Box) {
let chars = b.style.chars();
for col in (b.top_left.1 + 1)..b.bottom_right.1 {
if let Some(cell) = grid.get_mut(b.top_left.0, col) {
if !is_junction_char(*cell) {
*cell = chars.horizontal;
}
}
if let Some(cell) = grid.get_mut(b.bottom_right.0, col) {
if !is_junction_char(*cell) {
*cell = chars.horizontal;
}
}
}
for row in (b.top_left.0 + 1)..b.bottom_right.0 {
if let Some(cell) = grid.get_mut(row, b.top_left.1) {
if !is_junction_char(*cell) {
*cell = chars.vertical;
}
}
if let Some(cell) = grid.get_mut(row, b.bottom_right.1) {
if !is_junction_char(*cell) {
*cell = chars.vertical;
}
}
}
if let Some(cell) = grid.get_mut(b.top_left.0, b.top_left.1) {
*cell = chars.top_left;
}
if let Some(cell) = grid.get_mut(b.top_left.0, b.bottom_right.1) {
*cell = chars.top_right;
}
if let Some(cell) = grid.get_mut(b.bottom_right.0, b.top_left.1) {
*cell = chars.bottom_left;
}
if let Some(cell) = grid.get_mut(b.bottom_right.0, b.bottom_right.1) {
*cell = chars.bottom_right;
}
}
fn draw_text_row(grid: &mut Grid, row: &crate::primitives::TextRow) {
for (i, ch) in row.content.chars().enumerate() {
let col = row.start_col + i;
if col <= row.end_col {
if let Some(cell) = grid.get_mut(row.row, col) {
*cell = ch;
}
}
}
}
fn draw_horizontal_arrow(grid: &mut Grid, arrow: &crate::primitives::HorizontalArrow) {
let arrow_char = arrow.arrow_char.unwrap_or_else(|| {
let chars = arrow.arrow_type.chars();
if arrow.rightward {
chars.arrowhead_right
} else {
chars.arrowhead_left
}
});
for col in arrow.start_col..=arrow.end_col {
if let Some(cell) = grid.get_mut(arrow.row, col) {
if *cell == ' ' {
*cell = arrow_char;
}
}
}
}
fn draw_vertical_arrow(grid: &mut Grid, arrow: &crate::primitives::VerticalArrow) {
let default_char = if arrow.downward { '↓' } else { '↑' };
let arrow_char = arrow.arrow_char.unwrap_or(default_char);
if arrow.start_row == arrow.end_row {
if let Some(cell) = grid.get_mut(arrow.start_row, arrow.col) {
*cell = arrow_char;
}
return;
}
let is_filled_triangle = matches!(arrow_char, '▼' | '▲');
let connector_char = if is_filled_triangle {
'│'
} else {
arrow_char
};
let marker_row = if is_filled_triangle {
arrow.start_row + ((arrow.end_row - arrow.start_row) * 2 / 3)
} else {
arrow.start_row };
for row in arrow.start_row..=arrow.end_row {
if let Some(cell) = grid.get_mut(row, arrow.col) {
if *cell == ' ' {
if row == marker_row && is_filled_triangle {
*cell = arrow_char; } else {
*cell = connector_char; }
}
}
}
}
#[allow(dead_code)] #[allow(clippy::missing_const_for_fn)] fn draw_connection_line(grid: &mut Grid, conn: &crate::primitives::ConnectionLine) {
let mut junction_points = std::collections::HashSet::new();
if conn.segments.len() >= 2 {
for i in 0..conn.segments.len() - 1 {
let seg1 = &conn.segments[i];
let seg2 = &conn.segments[i + 1];
match (seg1, seg2) {
(
crate::primitives::Segment::Horizontal { row, end_col, .. },
crate::primitives::Segment::Vertical {
col: _,
start_row: _,
..
},
) => {
junction_points.insert((*row, *end_col));
}
(
crate::primitives::Segment::Vertical { col, end_row, .. },
crate::primitives::Segment::Horizontal {
row: _,
start_col: _,
..
},
) => {
junction_points.insert((*end_row, *col));
}
_ => {} }
}
}
for segment in &conn.segments {
match segment {
crate::primitives::Segment::Horizontal {
row,
start_col,
end_col,
} => {
for col in *start_col..=*end_col {
if !junction_points.contains(&(*row, col)) {
if let Some(current) = grid.get(*row, col) {
if current == ' ' || is_connection_line_char(current) {
if let Some(cell) = grid.get_mut(*row, col) {
*cell = '─';
}
}
}
}
}
}
crate::primitives::Segment::Vertical {
col,
start_row,
end_row,
} => {
for row in *start_row..=*end_row {
if !junction_points.contains(&(row, *col)) {
if let Some(current) = grid.get(row, *col) {
if current == ' ' || is_connection_line_char(current) {
if let Some(cell) = grid.get_mut(row, *col) {
*cell = '│';
}
}
}
}
}
}
}
}
draw_elbows_at_points(grid, conn, &junction_points);
}
const fn is_connection_line_char(c: char) -> bool {
matches!(
c,
'─' | '│' | '┌' | '┐' | '└' | '┘' | '├' | '┤' | '┬' | '┴' | '┼'
)
}
fn draw_elbows_at_points(
grid: &mut Grid,
conn: &crate::primitives::ConnectionLine,
junction_points: &std::collections::HashSet<(usize, usize)>,
) {
for &(row, col) in junction_points {
let mut horizontal_from_left = false;
let mut horizontal_from_right = false;
let mut vertical_from_above = false;
let mut vertical_from_below = false;
for segment in &conn.segments {
match segment {
crate::primitives::Segment::Horizontal {
row: seg_row,
start_col,
end_col,
} if *seg_row == row => {
if *start_col <= col && col <= *end_col {
if col == *start_col {
horizontal_from_right = true; } else if col == *end_col {
horizontal_from_left = true; }
}
}
crate::primitives::Segment::Vertical {
col: seg_col,
start_row,
end_row,
} if *seg_col == col => {
if *start_row <= row && row <= *end_row {
if row == *start_row {
vertical_from_above = true; } else if row == *end_row {
vertical_from_below = true; }
}
}
_ => {}
}
}
let elbow_char = match (
horizontal_from_left,
horizontal_from_right,
vertical_from_above,
vertical_from_below,
) {
(true, false, true, false) => '┌', (false, true, true, false) => '┐', (true, false, false, true) => '└', (false, true, false, true) => '┘', _ => continue, };
if grid.get(row, col) == Some(' ') {
if let Some(cell) = grid.get_mut(row, col) {
*cell = elbow_char;
}
}
}
}
#[allow(dead_code)] #[allow(clippy::missing_const_for_fn)] fn draw_label(grid: &mut Grid, label: &crate::primitives::Label) {
let final_col = match &label.attached_to {
crate::primitives::LabelAttachment::VerticalArrow(_) => {
if label.row > 0 {
let search_start = label.col.saturating_sub(3);
let search_end = (label.col + label.content.len() + 3).min(grid.width());
for col in search_start..search_end {
if let Some(ch) = grid.get(label.row - 1, col) {
if ch == '↓' || ch == '↑' {
let clean_content = label.content.trim_matches('"');
let formatted_content = format!(" {clean_content} ");
let label_width = formatted_content.chars().count();
let centered = col.saturating_sub(label_width / 2);
return draw_label_at_position(grid, label, label.row, centered.max(0));
}
}
}
}
label.col }
_ => label.col,
};
draw_label_at_position(grid, label, label.row, final_col);
}
fn draw_label_at_position(
grid: &mut Grid,
label: &crate::primitives::Label,
row: usize,
col: usize,
) {
let clean_content = label.content.trim_matches('"');
let formatted_content = format!(" {clean_content} ");
for (i, ch) in formatted_content.chars().enumerate() {
let target_col = col + i;
if let Some(cell) = grid.get_mut(row, target_col) {
*cell = ch;
}
}
}
#[test]
fn test_render_nested_different_styles() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (4, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: vec![1],
});
inventory.boxes.push(crate::primitives::Box {
top_left: (1, 2),
bottom_right: (3, 6),
style: BoxStyle::Double,
parent_idx: Some(0),
child_indices: Vec::new(),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
assert_eq!(grid.get(1, 2), Some('╔'));
}
#[test]
fn test_render_multiple_nested_levels() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (6, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: vec![1],
});
inventory.boxes.push(crate::primitives::Box {
top_left: (1, 1),
bottom_right: (5, 9),
style: BoxStyle::Double,
parent_idx: Some(0),
child_indices: vec![2],
});
inventory.boxes.push(crate::primitives::Box {
top_left: (2, 2),
bottom_right: (4, 8),
style: BoxStyle::Rounded,
parent_idx: Some(1),
child_indices: Vec::new(),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
assert_eq!(grid.get(1, 1), Some('╔'));
assert_eq!(grid.get(2, 2), Some('╭'));
}
#[test]
fn test_render_with_labels() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (2, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.labels.push(Label {
row: 4,
col: 0,
content: "Label".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (2, 0),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
assert_eq!(grid.get(4, 0), Some(' '));
assert_eq!(grid.get(4, 1), Some('L'));
assert_eq!(grid.get(4, 2), Some('a'));
assert_eq!(grid.get(4, 3), Some('b'));
assert_eq!(grid.get(4, 4), Some('e'));
assert!(grid.height() > 4);
}
#[test]
fn test_render_empty_labels() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (2, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
}
#[test]
fn test_render_label_no_collision() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (2, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.labels.push(Label {
row: 5,
col: 0,
content: "Text".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (3, 0),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
assert_eq!(grid.get(2, 4), Some('┘'));
assert_eq!(grid.get(5, 0), Some(' '));
assert_eq!(grid.get(5, 1), Some('T'));
assert_eq!(grid.get(5, 2), Some('e'));
assert_eq!(grid.get(5, 3), Some('x'));
assert_eq!(grid.get(5, 4), Some('t'));
}
#[test]
fn test_render_multiple_labels() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(crate::primitives::Box {
top_left: (0, 5),
bottom_right: (2, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.labels.push(Label {
row: 4,
col: 1,
content: "First".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (2, 0),
});
inventory.labels.push(Label {
row: 4,
col: 6,
content: "Second".to_string(),
attached_to: LabelAttachment::Box(1),
offset: (2, 0),
});
let grid = render_diagram(&inventory);
assert_eq!(grid.get(0, 0), Some('┌'));
assert_eq!(grid.get(0, 5), Some('┌'));
assert_eq!(grid.get(4, 1), Some(' '));
assert_eq!(grid.get(4, 2), Some('F'));
assert_eq!(grid.get(4, 3), Some('i'));
assert_eq!(grid.get(4, 4), Some('r'));
assert_eq!(grid.get(4, 5), Some('s'));
assert_eq!(grid.get(4, 6), Some(' '));
assert_eq!(grid.get(4, 7), Some('S'));
assert_eq!(grid.get(4, 8), Some('e'));
assert_eq!(grid.get(4, 9), Some('c'));
assert_eq!(grid.get(4, 10), Some('o'));
assert_eq!(grid.get(4, 11), Some('n'));
}