#[allow(unused_imports)] use crate::primitives::{
ArrowType, Box as DiagramBox, BoxStyle, HorizontalArrow, PrimitiveInventory,
};
#[must_use]
pub fn balance_horizontal_boxes(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
let groups = find_vertical_overlap_groups(&normalized);
for group in groups {
let max_width = group
.iter()
.map(|&idx| normalized.boxes[idx].width())
.max()
.unwrap_or(0);
for &idx in &group {
let box_ref = &mut normalized.boxes[idx];
let current_width = box_ref.width();
if current_width < max_width {
let diff = max_width - current_width;
box_ref.bottom_right.1 += diff;
}
}
}
normalized
}
#[allow(dead_code)] #[must_use]
fn find_vertical_overlap_groups(inventory: &PrimitiveInventory) -> Vec<Vec<usize>> {
if inventory.boxes.is_empty() {
return Vec::new();
}
let mut groups: Vec<Vec<usize>> = Vec::new();
let mut assigned = vec![false; inventory.boxes.len()];
for i in 0..inventory.boxes.len() {
if assigned[i] {
continue;
}
let mut group = vec![i];
assigned[i] = true;
let mut changed = true;
while changed {
changed = false;
let mut to_add = Vec::new();
for (j, &is_assigned) in assigned.iter().enumerate() {
if is_assigned {
continue;
}
let mut is_adjacent = false;
for &group_idx in &group {
let box_in_group = &inventory.boxes[group_idx];
let box_j = &inventory.boxes[j];
let rows_overlap = !(box_in_group.bottom_right.0 < box_j.top_left.0
|| box_j.bottom_right.0 < box_in_group.top_left.0);
let (left_col, right_col) = if box_in_group.bottom_right.1 < box_j.top_left.1 {
(box_in_group.bottom_right.1, box_j.top_left.1)
} else if box_j.bottom_right.1 < box_in_group.top_left.1 {
(box_j.bottom_right.1, box_in_group.top_left.1)
} else {
continue; };
let gap = right_col.saturating_sub(left_col);
if rows_overlap && gap <= 1 {
is_adjacent = true;
break;
}
}
if is_adjacent {
to_add.push(j);
}
}
for j in to_add {
group.push(j);
assigned[j] = true;
changed = true;
}
}
if group.len() > 1 {
groups.push(group);
}
}
groups
}
#[allow(dead_code)] pub fn align_horizontal_arrows(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
let mut arrows_by_row = std::collections::BTreeMap::new();
for arrow in &normalized.horizontal_arrows {
arrows_by_row
.entry(arrow.row)
.or_insert_with(Vec::new)
.push(arrow.clone());
}
normalized.horizontal_arrows = arrows_by_row
.into_iter()
.flat_map(|(_row, mut arrows)| {
arrows.sort_by_key(|a| a.start_col);
arrows
})
.collect();
normalized
}
#[allow(dead_code)] #[must_use]
pub fn normalize_padding(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
for row in &mut normalized.text_rows {
let containing_boxes: Vec<_> = normalized
.boxes
.iter()
.filter(|box_| {
row.row > box_.top_left.0
&& row.row < box_.bottom_right.0
&& row.start_col >= box_.top_left.1
&& row.start_col <= box_.bottom_right.1
})
.collect();
if let Some(b) = containing_boxes
.iter()
.min_by_key(|box_| box_.width() * box_.height())
{
row.start_col = b.top_left.1 + 1;
row.end_col = b.bottom_right.1 - 1;
}
}
normalized
}
#[allow(dead_code)] #[must_use]
pub fn align_vertical_arrows(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
for arrow in &mut normalized.vertical_arrows {
if let Some(aligned_col) = find_alignment_column(&normalized.boxes, arrow.col) {
arrow.col = aligned_col;
}
}
normalized
}
fn find_alignment_column(boxes: &[crate::primitives::Box], arrow_col: usize) -> Option<usize> {
if boxes.is_empty() {
return None;
}
let mut best_box: Option<&crate::primitives::Box> = None;
let mut best_score = usize::MAX;
for b in boxes {
let box_center = usize::midpoint(b.top_left.1, b.bottom_right.1);
let distance_to_center = box_center.abs_diff(arrow_col);
let arrow_in_box_range = arrow_col >= b.top_left.1 && arrow_col <= b.bottom_right.1;
let score = distance_to_center + if arrow_in_box_range { 0 } else { 10 };
if score < best_score {
best_score = score;
best_box = Some(b);
}
}
best_box.map(|b| usize::midpoint(b.top_left.1, b.bottom_right.1))
}
#[allow(dead_code)] #[must_use]
pub fn normalize_box_widths(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
for b in &mut normalized.boxes {
let max_content_len = normalized
.text_rows
.iter()
.filter(|row| {
row.row > b.top_left.0
&& row.row < b.bottom_right.0
&& row.start_col >= b.top_left.1
&& row.start_col <= b.bottom_right.1
})
.map(|row| row.content.trim_end().len())
.max()
.unwrap_or(0);
if max_content_len > 0 {
let required_width = max_content_len + 2;
let current_width = b.width();
if required_width > current_width {
let expansion = required_width - current_width;
b.bottom_right.1 += expansion;
}
}
}
for row in &mut normalized.text_rows {
if let Some(b) = normalized.boxes.iter().find(|box_| {
row.row > box_.top_left.0
&& row.row < box_.bottom_right.0
&& row.start_col >= box_.top_left.1
&& row.start_col <= box_.bottom_right.1
}) {
row.end_col = b.bottom_right.1 - 1;
}
}
normalized
}
#[allow(dead_code)] #[must_use]
pub fn normalize_nested_boxes(inventory: &PrimitiveInventory) -> PrimitiveInventory {
let mut normalized = inventory.clone();
let mut parent_child_groups = Vec::new();
for (parent_idx, parent) in normalized.boxes.iter().enumerate() {
let mut children = Vec::new();
for (child_idx, child) in normalized.boxes.iter().enumerate() {
if child_idx != parent_idx {
let child_contained = 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 child_contained {
children.push(child_idx);
}
}
}
if !children.is_empty() {
parent_child_groups.push((parent_idx, children));
}
}
for (parent_idx, children) in parent_child_groups {
let mut required_right = normalized.boxes[parent_idx].bottom_right.1;
let mut required_bottom = normalized.boxes[parent_idx].bottom_right.0;
for &child_idx in &children {
let child = &normalized.boxes[child_idx];
required_right = required_right.max(child.bottom_right.1 + 2);
required_bottom = required_bottom.max(child.bottom_right.0 + 2);
}
let current_right = normalized.boxes[parent_idx].bottom_right.1;
let current_bottom = normalized.boxes[parent_idx].bottom_right.0;
if required_right > current_right {
normalized.boxes[parent_idx].bottom_right.1 = required_right;
}
if required_bottom > current_bottom {
normalized.boxes[parent_idx].bottom_right.0 = required_bottom;
}
}
let mut parent_child_groups = Vec::new();
for (parent_idx, parent) in normalized.boxes.iter().enumerate() {
let mut children = Vec::new();
for (child_idx, child) in normalized.boxes.iter().enumerate() {
if child_idx != parent_idx {
let child_contained = 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 child_contained {
children.push(child_idx);
}
}
}
if !children.is_empty() {
parent_child_groups.push((parent_idx, children));
}
}
for (parent_idx, children) in parent_child_groups {
let mut required_right = normalized.boxes[parent_idx].bottom_right.1;
let mut required_bottom = normalized.boxes[parent_idx].bottom_right.0;
for &child_idx in &children {
let child = &normalized.boxes[child_idx];
required_right = required_right.max(child.bottom_right.1 + 2);
required_bottom = required_bottom.max(child.bottom_right.0 + 2);
}
let current_right = normalized.boxes[parent_idx].bottom_right.1;
let current_bottom = normalized.boxes[parent_idx].bottom_right.0;
if required_right > current_right {
normalized.boxes[parent_idx].bottom_right.1 = required_right;
}
if required_bottom > current_bottom {
normalized.boxes[parent_idx].bottom_right.0 = required_bottom;
}
}
let mut expansions = Vec::new();
for i in 0..normalized.boxes.len() {
if let Some(parent_idx) = normalized.boxes[i].parent_idx {
let child = &normalized.boxes[i];
let required_right = child.bottom_right.1 + 2;
let required_bottom = child.bottom_right.0 + 2;
expansions.push((parent_idx, required_right, required_bottom));
}
}
for (parent_idx, required_right, required_bottom) in expansions {
let parent = &mut normalized.boxes[parent_idx];
if required_right > parent.bottom_right.1 {
parent.bottom_right.1 = required_right;
}
if required_bottom > parent.bottom_right.0 {
parent.bottom_right.0 = required_bottom;
}
}
normalized
}
#[allow(dead_code)] #[must_use]
pub fn normalize_connection_lines(inventory: &PrimitiveInventory) -> PrimitiveInventory {
inventory.clone()
}
#[allow(dead_code)] #[must_use]
pub fn normalize_labels(inventory: &PrimitiveInventory) -> PrimitiveInventory {
inventory.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_narrow_box() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 2,
content: " Longer Text ".to_string(),
});
let normalized = normalize_box_widths(&inventory);
let b = &normalized.boxes[0];
assert!(b.width() >= 13);
}
#[test]
fn test_box_with_empty_content_unchanged() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let normalized = normalize_box_widths(&inventory);
let b = &normalized.boxes[0];
assert_eq!(b.width(), 4); }
#[test]
fn test_multiple_boxes_expanded_independently() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (2, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 2,
content: " Short ".to_string(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 6,
end_col: 7,
content: " Much Longer Text ".to_string(),
});
let normalized = normalize_box_widths(&inventory);
assert!(normalized.boxes[0].width() >= 7);
assert!(normalized.boxes[1].width() >= 18);
}
#[test]
fn test_box_width_already_sufficient() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 9,
content: " Short ".to_string(),
});
let normalized = normalize_box_widths(&inventory);
let b = &normalized.boxes[0];
assert_eq!(b.width(), 11); }
#[test]
fn test_text_rows_adjusted_after_expansion() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 2,
content: " Expanded ".to_string(),
});
let normalized = normalize_box_widths(&inventory);
let row = &normalized.text_rows[0];
assert_eq!(row.end_col, normalized.boxes[0].bottom_right.1 - 1);
}
#[test]
fn test_multiline_box_uses_longest_row() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (4, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 2,
content: " A ".to_string(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 2,
start_col: 1,
end_col: 2,
content: " LongerLine ".to_string(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 3,
start_col: 1,
end_col: 2,
content: " B ".to_string(),
});
let normalized = normalize_box_widths(&inventory);
let b = &normalized.boxes[0];
assert!(b.width() >= 12);
}
#[test]
fn test_align_single_arrow() {
let mut inventory = PrimitiveInventory::default();
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 2,
end_col: 5,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
let normalized = align_horizontal_arrows(&inventory);
assert_eq!(normalized.horizontal_arrows.len(), 1);
assert_eq!(normalized.horizontal_arrows[0].start_col, 2);
}
#[test]
fn test_align_multiple_arrows_same_row() {
let mut inventory = PrimitiveInventory::default();
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 10,
end_col: 15,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 2,
end_col: 5,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
let normalized = align_horizontal_arrows(&inventory);
assert_eq!(normalized.horizontal_arrows.len(), 2);
assert!(
normalized.horizontal_arrows[0].start_col < normalized.horizontal_arrows[1].start_col
);
}
#[test]
fn test_arrows_different_rows_unchanged() {
let mut inventory = PrimitiveInventory::default();
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 5,
end_col: 8,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
inventory.horizontal_arrows.push(HorizontalArrow {
row: 5,
start_col: 5,
end_col: 8,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
let normalized = align_horizontal_arrows(&inventory);
assert_eq!(normalized.horizontal_arrows.len(), 2);
assert_eq!(normalized.horizontal_arrows[0].row, 0);
assert_eq!(normalized.horizontal_arrows[1].row, 5);
}
#[test]
fn test_no_arrows_handled_gracefully() {
let inventory = PrimitiveInventory::default();
let normalized = align_horizontal_arrows(&inventory);
assert!(normalized.horizontal_arrows.is_empty());
}
#[test]
fn test_align_vertical_arrow_to_box_center() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory
.vertical_arrows
.push(crate::primitives::VerticalArrow {
col: 11,
start_row: 4,
end_row: 6,
arrow_type: ArrowType::Standard,
downward: true,
arrow_char: None,
});
let normalized = align_vertical_arrows(&inventory);
let arrow = &normalized.vertical_arrows[0];
assert_eq!(arrow.col, 10);
}
#[test]
fn test_align_vertical_arrow_to_box_edge() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory
.vertical_arrows
.push(crate::primitives::VerticalArrow {
col: 6,
start_row: 4,
end_row: 6,
arrow_type: ArrowType::Standard,
downward: true,
arrow_char: None,
});
let normalized = align_vertical_arrows(&inventory);
let arrow = &normalized.vertical_arrows[0];
assert_eq!(arrow.col, 10);
}
#[test]
fn test_align_vertical_arrow_to_nearest_box() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 2),
bottom_right: (3, 5),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 10),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory
.vertical_arrows
.push(crate::primitives::VerticalArrow {
col: 9,
start_row: 4,
end_row: 6,
arrow_type: ArrowType::Standard,
downward: true,
arrow_char: None,
});
let normalized = align_vertical_arrows(&inventory);
let arrow = &normalized.vertical_arrows[0];
assert!(arrow.col >= 10 && arrow.col <= 15);
}
#[test]
fn test_vertical_arrow_maintains_row_positions() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory
.vertical_arrows
.push(crate::primitives::VerticalArrow {
col: 11,
start_row: 4,
end_row: 6,
arrow_type: ArrowType::Standard,
downward: true,
arrow_char: None,
});
let normalized = align_vertical_arrows(&inventory);
let arrow = &normalized.vertical_arrows[0];
assert_eq!(arrow.start_row, 4);
assert_eq!(arrow.end_row, 6);
}
#[test]
fn test_no_vertical_arrows_unchanged() {
let inventory = PrimitiveInventory::default();
let normalized = align_vertical_arrows(&inventory);
assert!(normalized.vertical_arrows.is_empty());
}
#[test]
fn test_padding_enforces_one_space_inside_box() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 0,
end_col: 9,
content: "Content".to_string(),
});
let normalized = normalize_padding(&inventory);
let row = &normalized.text_rows[0];
assert_eq!(row.start_col, 1);
}
#[test]
fn test_padding_enforces_uniform_one_space() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 5,
end_col: 14,
content: "Row1".to_string(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 2,
start_col: 7,
end_col: 14,
content: "Row2".to_string(),
});
let normalized = normalize_padding(&inventory);
assert_eq!(normalized.text_rows[0].start_col, 6); assert_eq!(normalized.text_rows[1].start_col, 6); }
#[test]
fn test_padding_respects_box_boundaries() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 2),
bottom_right: (3, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 2,
end_col: 7,
content: "Text".to_string(),
});
let normalized = normalize_padding(&inventory);
let row = &normalized.text_rows[0];
assert_eq!(row.start_col, 3);
assert_eq!(row.end_col, 7);
}
#[test]
fn test_padding_no_rows_no_crash() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let normalized = normalize_padding(&inventory);
assert!(normalized.text_rows.is_empty());
}
#[test]
fn test_padding_multiple_boxes_independent() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 5),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 10),
bottom_right: (2, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 0,
end_col: 4,
content: "A".to_string(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 10,
end_col: 14,
content: "B".to_string(),
});
let normalized = normalize_padding(&inventory);
assert_eq!(normalized.text_rows[0].start_col, 1);
assert_eq!(normalized.text_rows[1].start_col, 11);
}
#[test]
fn test_normalization_idempotent_box_widths() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 1,
end_col: 2,
content: " LongContent ".to_string(),
});
let normalized1 = normalize_box_widths(&inventory);
let normalized2 = normalize_box_widths(&normalized1);
assert_eq!(normalized1.boxes, normalized2.boxes);
assert_eq!(normalized1.text_rows, normalized2.text_rows);
}
#[test]
fn test_normalization_idempotent_horizontal_arrows() {
let mut inventory = PrimitiveInventory::default();
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 2,
end_col: 5,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
inventory.horizontal_arrows.push(HorizontalArrow {
row: 0,
start_col: 10,
end_col: 15,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
let normalized1 = align_horizontal_arrows(&inventory);
let normalized2 = align_horizontal_arrows(&normalized1);
assert_eq!(normalized1.horizontal_arrows, normalized2.horizontal_arrows);
}
#[test]
fn test_normalization_idempotent_vertical_arrows() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 15),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory
.vertical_arrows
.push(crate::primitives::VerticalArrow {
col: 11,
start_row: 4,
end_row: 6,
arrow_type: ArrowType::Standard,
downward: true,
arrow_char: None,
});
let normalized1 = align_vertical_arrows(&inventory);
let normalized2 = align_vertical_arrows(&normalized1);
assert_eq!(normalized1.vertical_arrows, normalized2.vertical_arrows);
}
#[test]
fn test_normalization_idempotent_padding() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 0,
end_col: 9,
content: "Content".to_string(),
});
let normalized1 = normalize_padding(&inventory);
let normalized2 = normalize_padding(&normalized1);
assert_eq!(normalized1.text_rows, normalized2.text_rows);
}
#[test]
fn test_full_normalization_pipeline_idempotent() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 5),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.text_rows.push(crate::primitives::TextRow {
row: 1,
start_col: 0,
end_col: 4,
content: " Text ".to_string(),
});
inventory.horizontal_arrows.push(HorizontalArrow {
row: 3,
start_col: 0,
end_col: 2,
arrow_type: ArrowType::Standard,
rightward: true,
arrow_char: None,
});
let step1 = normalize_box_widths(&inventory);
let step2 = normalize_padding(&step1);
let step3 = align_horizontal_arrows(&step2);
let step4 = align_vertical_arrows(&step3);
let step1b = normalize_box_widths(&step4);
let step2b = normalize_padding(&step1b);
let step3b = align_horizontal_arrows(&step2b);
let step4b = align_vertical_arrows(&step3b);
assert_eq!(step4.boxes, step4b.boxes);
assert_eq!(step4.text_rows, step4b.text_rows);
assert_eq!(step4.horizontal_arrows, step4b.horizontal_arrows);
assert_eq!(step4.vertical_arrows, step4b.vertical_arrows);
}
#[test]
fn test_find_vertical_overlap_groups_single_box() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 5),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let groups = find_vertical_overlap_groups(&inventory);
assert!(groups.is_empty() || groups.iter().all(|g| g.len() <= 1));
}
#[test]
fn test_find_vertical_overlap_groups_separate_boxes() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (5, 0),
bottom_right: (7, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let groups = find_vertical_overlap_groups(&inventory);
assert!(groups.is_empty() || groups.iter().all(|g| g.len() == 1));
}
#[test]
fn test_find_vertical_overlap_groups_side_by_side() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 5),
bottom_right: (3, 9),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let groups = find_vertical_overlap_groups(&inventory);
assert!(groups.iter().any(|g| g.len() == 2));
}
#[test]
fn test_find_vertical_overlap_groups_three_boxes() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 2),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 3),
bottom_right: (2, 5),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 6),
bottom_right: (2, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let groups = find_vertical_overlap_groups(&inventory);
assert!(groups.iter().any(|g| g.len() == 3));
}
#[test]
fn test_find_vertical_overlap_groups_stacked_not_grouped() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (3, 0),
bottom_right: (5, 3),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let groups = find_vertical_overlap_groups(&inventory);
assert!(groups.is_empty() || groups.iter().all(|g| g.len() == 1));
}
#[test]
fn test_balance_horizontal_boxes_equalizes_widths() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 2),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 3),
bottom_right: (2, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let balanced = balance_horizontal_boxes(&inventory);
assert_eq!(balanced.boxes[0].width(), 6);
assert_eq!(balanced.boxes[1].width(), 6);
}
#[test]
fn test_balance_horizontal_boxes_idempotent() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 2),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 3),
bottom_right: (2, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let balanced1 = balance_horizontal_boxes(&inventory);
let balanced2 = balance_horizontal_boxes(&balanced1);
assert_eq!(balanced1.boxes, balanced2.boxes);
}
#[test]
fn test_normalize_connection_lines_empty() {
let inventory = PrimitiveInventory::default();
let normalized = normalize_connection_lines(&inventory);
assert!(normalized.connection_lines.is_empty());
}
#[test]
fn test_normalize_connection_lines_preserves_basic() {
use crate::primitives::{ConnectionLine, Segment};
let mut inventory = PrimitiveInventory::default();
inventory.connection_lines.push(ConnectionLine {
segments: vec![Segment::Horizontal {
row: 2,
start_col: 0,
end_col: 5,
}],
from_box: Some(0),
to_box: None,
});
let normalized = normalize_connection_lines(&inventory);
assert_eq!(normalized.connection_lines.len(), 1);
assert_eq!(normalized.connection_lines[0].segments.len(), 1);
}
#[test]
fn test_normalize_connection_lines_idempotent() {
use crate::primitives::{ConnectionLine, Segment};
let mut inventory = PrimitiveInventory::default();
inventory.connection_lines.push(ConnectionLine {
segments: vec![
Segment::Horizontal {
row: 2,
start_col: 0,
end_col: 5,
},
Segment::Vertical {
col: 5,
start_row: 2,
end_row: 5,
},
],
from_box: Some(0),
to_box: Some(1),
});
let norm1 = normalize_connection_lines(&inventory);
let norm2 = normalize_connection_lines(&norm1);
assert_eq!(norm1.connection_lines, norm2.connection_lines);
}
#[test]
fn test_normalize_nested_boxes_empty() {
let inventory = PrimitiveInventory::default();
let normalized = normalize_nested_boxes(&inventory);
assert!(normalized.boxes.is_empty());
}
#[test]
fn test_normalize_nested_boxes_no_nesting() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
let normalized = normalize_nested_boxes(&inventory);
assert_eq!(normalized.boxes.len(), 1);
assert_eq!(normalized.boxes[0].bottom_right, (2, 4));
}
#[test]
fn test_normalize_nested_boxes_expands_parent() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (3, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: vec![1],
});
inventory.boxes.push(DiagramBox {
top_left: (1, 2),
bottom_right: (2, 6),
style: BoxStyle::Single,
parent_idx: Some(0),
child_indices: Vec::new(),
});
let normalized = normalize_nested_boxes(&inventory);
assert!(normalized.boxes[0].bottom_right.1 >= 6);
}
#[test]
fn test_normalize_nested_boxes_multiple_children() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (6, 6),
style: BoxStyle::Single,
parent_idx: None,
child_indices: vec![1, 2],
});
inventory.boxes.push(DiagramBox {
top_left: (1, 1),
bottom_right: (2, 3),
style: BoxStyle::Single,
parent_idx: Some(0),
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (4, 4),
bottom_right: (5, 8),
style: BoxStyle::Single,
parent_idx: Some(0),
child_indices: Vec::new(),
});
let normalized = normalize_nested_boxes(&inventory);
assert!(normalized.boxes[0].bottom_right.1 >= 8);
}
#[test]
fn test_normalize_nested_boxes_idempotent() {
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (4, 8),
style: BoxStyle::Single,
parent_idx: None,
child_indices: vec![1],
});
inventory.boxes.push(DiagramBox {
top_left: (1, 2),
bottom_right: (3, 6),
style: BoxStyle::Single,
parent_idx: Some(0),
child_indices: Vec::new(),
});
let norm1 = normalize_nested_boxes(&inventory);
let norm2 = normalize_nested_boxes(&norm1);
assert_eq!(norm1.boxes, norm2.boxes);
}
#[test]
fn test_normalize_labels_empty() {
let inventory = PrimitiveInventory::default();
let normalized = normalize_labels(&inventory);
assert!(normalized.labels.is_empty());
}
#[test]
fn test_normalize_labels_no_attachment() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.labels.push(Label {
row: 5,
col: 10,
content: "Text".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (0, 0),
});
let normalized = normalize_labels(&inventory);
assert_eq!(normalized.labels.len(), 1);
}
#[test]
fn test_normalize_labels_preserves_offset() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
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: 2,
content: "Label".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (3, 0),
});
let normalized = normalize_labels(&inventory);
assert_eq!(normalized.labels[0].offset, (3, 0));
}
#[test]
fn test_normalize_labels_idempotent() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
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: 2,
content: "Label".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (3, 0),
});
let norm1 = normalize_labels(&inventory);
let norm2 = normalize_labels(&norm1);
assert_eq!(norm1.labels, norm2.labels);
}
#[test]
fn test_normalize_labels_multiple() {
use crate::primitives::{Label, LabelAttachment};
let mut inventory = PrimitiveInventory::default();
inventory.boxes.push(DiagramBox {
top_left: (0, 0),
bottom_right: (2, 4),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.boxes.push(DiagramBox {
top_left: (0, 6),
bottom_right: (2, 10),
style: BoxStyle::Single,
parent_idx: None,
child_indices: Vec::new(),
});
inventory.labels.push(Label {
row: 4,
col: 2,
content: "First".to_string(),
attached_to: LabelAttachment::Box(0),
offset: (2, 0),
});
inventory.labels.push(Label {
row: 4,
col: 8,
content: "Second".to_string(),
attached_to: LabelAttachment::Box(1),
offset: (2, 0),
});
let normalized = normalize_labels(&inventory);
assert_eq!(normalized.labels.len(), 2);
}
}