pub use self::arrows::{detect_horizontal_arrows, detect_vertical_arrows};
pub use self::boxes::detect_boxes;
mod arrows;
mod boxes;
#[allow(dead_code)] #[must_use]
pub fn detect_all_primitives(grid: &crate::grid::Grid) -> crate::primitives::PrimitiveInventory {
let boxes = detect_boxes(grid);
let horizontal_arrows = detect_horizontal_arrows(grid);
let vertical_arrows = detect_vertical_arrows(grid);
let temp_inventory = crate::primitives::PrimitiveInventory {
boxes: boxes.clone(),
horizontal_arrows: horizontal_arrows.clone(),
vertical_arrows: vertical_arrows.clone(),
text_rows: Vec::new(),
connection_lines: Vec::new(),
labels: Vec::new(),
};
let labels = detect_labels(grid, &temp_inventory);
let mut text_rows = Vec::new();
let has_nested_boxes = boxes.len() > 1
&& boxes.iter().any(|b1| {
boxes.iter().any(|b2| {
b1 != b2
&& b1.top_left.0 < b2.top_left.0
&& b1.bottom_right.0 > b2.bottom_right.0
&& b1.top_left.1 < b2.top_left.1
&& b1.bottom_right.1 > b2.bottom_right.1
})
});
if has_nested_boxes {
} else {
for b in &boxes {
for (line_idx, line) in extract_box_content(grid, b).iter().enumerate() {
if !line.trim().is_empty() {
let interior_row = b.top_left.0 + 1 + line_idx;
let clean_content = line.trim_end_matches(|c| ['║', '│', '┃'].contains(&c));
text_rows.push(crate::primitives::TextRow {
row: interior_row,
start_col: b.top_left.1 + 1,
end_col: b.bottom_right.1 - 1,
content: clean_content.to_string(),
});
}
}
}
}
let boxes = establish_parent_child_relationships(boxes);
crate::primitives::PrimitiveInventory {
boxes,
horizontal_arrows,
vertical_arrows,
text_rows,
connection_lines: Vec::new(),
labels,
}
}
fn establish_parent_child_relationships(
mut boxes: Vec<crate::primitives::Box>,
) -> Vec<crate::primitives::Box> {
for i in 0..boxes.len() {
for j in 0..boxes.len() {
if i != j {
let parent = &boxes[i];
let child = &boxes[j];
let contains = child.top_left.0 > parent.top_left.0 && child.bottom_right.0 < parent.bottom_right.0 && child.top_left.1 > parent.top_left.1 && child.bottom_right.1 < parent.bottom_right.1;
if contains {
boxes[j].parent_idx = Some(i);
if !boxes[i].child_indices.contains(&j) {
boxes[i].child_indices.push(j);
}
}
}
}
}
boxes
}
#[allow(dead_code)] #[must_use]
pub fn extract_box_content(grid: &crate::grid::Grid, b: &crate::primitives::Box) -> Vec<String> {
let mut content = Vec::new();
for row in (b.top_left.0 + 1)..b.bottom_right.0 {
let mut line = String::new();
for col in (b.top_left.1 + 1)..b.bottom_right.1 {
if let Some(ch) = grid.get(row, col) {
line.push(ch);
}
}
content.push(line);
}
content
}
const fn is_box_char(ch: char) -> bool {
matches!(
ch,
'─' | '│'
| '┌'
| '┐'
| '└'
| '┘'
| '├'
| '┤'
| '┼'
| '┬'
| '┴'
| '┃'
| '═'
| '║'
| '╔'
| '╗'
| '╚'
| '╝'
| '╭'
| '╮'
| '╰'
| '╯'
)
}
#[allow(dead_code)] #[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn detect_labels(
grid: &crate::grid::Grid,
inventory: &crate::primitives::PrimitiveInventory,
) -> Vec<crate::primitives::Label> {
let mut labels = Vec::new();
let mut occupied_positions = std::collections::HashSet::new();
for b in &inventory.boxes {
for row in b.top_left.0..=b.bottom_right.0 {
for col in b.top_left.1..=b.bottom_right.1 {
occupied_positions.insert((row, col));
}
}
}
for arrow in &inventory.horizontal_arrows {
for col in arrow.start_col..=arrow.end_col {
occupied_positions.insert((arrow.row, col));
}
}
for arrow in &inventory.vertical_arrows {
for row in arrow.start_row..=arrow.end_row {
occupied_positions.insert((row, arrow.col));
}
}
for text_row in &inventory.text_rows {
for i in 0..text_row.content.len() {
let col = text_row.start_col + i;
if col <= text_row.end_col {
occupied_positions.insert((text_row.row, col));
}
}
}
for row in 0..grid.height() {
let mut current_label_start = None;
let mut current_label_text = String::new();
for col in 0..grid.width() {
if let Some(ch) = grid.get(row, col) {
if !occupied_positions.contains(&(row, col)) && !is_box_char(ch) && ch != ' ' {
if current_label_start.is_none() {
current_label_start = Some(col);
}
current_label_text.push(ch);
} else {
if let Some(start_col) = current_label_start {
if !current_label_text.is_empty() && current_label_text.len() > 1 {
if let Some(attachment) =
find_nearest_primitive(row, start_col, inventory)
{
labels.push(crate::primitives::Label {
row,
col: start_col,
content: current_label_text.clone(),
attached_to: attachment,
offset: calculate_offset(
row,
start_col,
&attachment,
inventory,
),
});
}
}
}
current_label_start = None;
current_label_text.clear();
}
}
}
if let Some(start_col) = current_label_start {
if !current_label_text.is_empty() && current_label_text.len() > 1 {
if let Some(attachment) = find_nearest_primitive(row, start_col, inventory) {
labels.push(crate::primitives::Label {
row,
col: start_col,
content: current_label_text,
attached_to: attachment,
offset: calculate_offset(row, start_col, &attachment, inventory),
});
}
}
}
}
labels
}
fn find_nearest_primitive(
row: usize,
col: usize,
inventory: &crate::primitives::PrimitiveInventory,
) -> Option<crate::primitives::LabelAttachment> {
let mut nearest: Option<(crate::primitives::LabelAttachment, usize)> = None;
for (idx, b) in inventory.boxes.iter().enumerate() {
let distances = [
if row < b.top_left.0 {
b.top_left.0 - row
} else {
row.saturating_sub(b.bottom_right.0)
},
if row > b.bottom_right.0 {
row - b.bottom_right.0
} else {
b.top_left.0.saturating_sub(row)
},
if col < b.top_left.1 {
b.top_left.1 - col
} else {
col.saturating_sub(b.bottom_right.1)
},
if col > b.bottom_right.1 {
col - b.bottom_right.1
} else {
b.top_left.1.saturating_sub(col)
},
];
let min_distance = distances.iter().min().unwrap();
if *min_distance <= 2 {
if let Some((_, current_min)) = nearest {
if *min_distance < current_min {
nearest = Some((crate::primitives::LabelAttachment::Box(idx), *min_distance));
}
} else {
nearest = Some((crate::primitives::LabelAttachment::Box(idx), *min_distance));
}
}
}
for (idx, arrow) in inventory.vertical_arrows.iter().enumerate() {
let col_distance = arrow.col.abs_diff(col);
if col_distance <= 4 {
let arrow_priority = col_distance;
if let Some((current_attachment, current_min)) = nearest {
let should_prefer_arrow = match current_attachment {
crate::primitives::LabelAttachment::Box(_) => {
arrow_priority <= 3 }
_ => arrow_priority < current_min,
};
if should_prefer_arrow {
nearest = Some((
crate::primitives::LabelAttachment::VerticalArrow(idx),
arrow_priority,
));
}
} else {
nearest = Some((
crate::primitives::LabelAttachment::VerticalArrow(idx),
arrow_priority,
));
}
}
}
nearest.map(|(attachment, _)| attachment)
}
#[allow(clippy::cast_possible_wrap)] fn calculate_offset(
row: usize,
col: usize,
attachment: &crate::primitives::LabelAttachment,
inventory: &crate::primitives::PrimitiveInventory,
) -> (isize, isize) {
match attachment {
crate::primitives::LabelAttachment::Box(idx) => {
inventory.boxes.get(*idx).map_or((0, 0), |b| {
let center_row = usize::midpoint(b.top_left.0, b.bottom_right.0);
let center_col = usize::midpoint(b.top_left.1, b.bottom_right.1);
(
row as isize - center_row as isize,
col as isize - center_col as isize,
)
})
}
_ => (0, 0), }
}