use crate::layout::Grid;
const MAX_NUDGE_DISTANCE: usize = 3;
const MIN_SEGMENT_LEN_FOR_NUDGE: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Axis {
Horizontal,
Vertical,
}
#[derive(Debug, Clone)]
struct Segment {
edge_idx: usize,
axis: Axis,
fixed_coord: usize,
range: (usize, usize),
path_idx_range: (usize, usize),
}
#[derive(Debug, Clone)]
struct Shift {
edge_idx: usize,
new_path: Vec<(usize, usize)>,
}
pub(crate) fn run(
grid: &mut Grid,
paths: &mut [Option<Vec<(usize, usize)>>],
edge_is_back: &[bool],
tip_for: impl Fn(usize) -> char,
) {
let segments = collect_segments(paths, edge_is_back);
let shifts = plan_parallel_merges(&segments, paths, grid);
apply_shifts(grid, paths, shifts, &tip_for);
}
fn collect_segments(
paths: &[Option<Vec<(usize, usize)>>],
edge_is_back: &[bool],
) -> Vec<Segment> {
let mut out = Vec::new();
for (edge_idx, path_opt) in paths.iter().enumerate() {
let Some(path) = path_opt else { continue };
if !edge_is_back.get(edge_idx).copied().unwrap_or(false) {
continue;
}
if path.len() < 2 {
continue;
}
let mut start_idx = 0usize;
let mut current_axis = step_axis(path, 0);
for i in 1..path.len() - 1 {
let next_axis = step_axis(path, i);
if next_axis != current_axis {
emit_segment(path, edge_idx, current_axis, start_idx, i, &mut out);
start_idx = i;
current_axis = next_axis;
}
}
emit_segment(
path,
edge_idx,
current_axis,
start_idx,
path.len() - 1,
&mut out,
);
}
out
}
fn step_axis(path: &[(usize, usize)], i: usize) -> Axis {
let (c, r) = path[i];
let (nc, nr) = path[i + 1];
if c == nc {
Axis::Vertical
} else if r == nr {
Axis::Horizontal
} else {
Axis::Horizontal
}
}
fn emit_segment(
path: &[(usize, usize)],
edge_idx: usize,
axis: Axis,
start_idx: usize,
end_idx: usize,
out: &mut Vec<Segment>,
) {
if end_idx <= start_idx {
return;
}
let (start_c, start_r) = path[start_idx];
let (end_c, end_r) = path[end_idx];
let (fixed_coord, range) = match axis {
Axis::Horizontal => (start_r, (start_c.min(end_c), start_c.max(end_c))),
Axis::Vertical => (start_c, (start_r.min(end_r), start_r.max(end_r))),
};
out.push(Segment {
edge_idx,
axis,
fixed_coord,
range,
path_idx_range: (start_idx, end_idx),
});
}
fn plan_parallel_merges(
segments: &[Segment],
paths: &[Option<Vec<(usize, usize)>>],
grid: &Grid,
) -> Vec<Shift> {
let mut shifts = Vec::new();
let horizontals: Vec<&Segment> = segments
.iter()
.filter(|s| s.axis == Axis::Horizontal)
.filter(|s| s.range.1 - s.range.0 + 1 >= MIN_SEGMENT_LEN_FOR_NUDGE)
.collect();
let mut already_shifted: std::collections::HashSet<usize> =
std::collections::HashSet::new();
for i in 0..horizontals.len() {
for j in (i + 1)..horizontals.len() {
let (a, b) = (horizontals[i], horizontals[j]);
if a.edge_idx == b.edge_idx {
continue;
}
if already_shifted.contains(&a.edge_idx)
|| already_shifted.contains(&b.edge_idx)
{
continue;
}
let row_delta = a.fixed_coord.abs_diff(b.fixed_coord);
if row_delta == 0 || row_delta > MAX_NUDGE_DISTANCE {
continue;
}
let (a_lo, a_hi) = a.range;
let (b_lo, b_hi) = b.range;
if a_hi < b_lo || b_hi < a_lo {
continue;
}
let target_row = a.fixed_coord.max(b.fixed_coord);
let source_seg = if a.fixed_coord < b.fixed_coord { a } else { b };
let Some(old_path) = paths[source_seg.edge_idx].as_ref() else {
continue;
};
let new_path = build_shifted_path(old_path, source_seg, target_row);
if !path_is_feasible(grid, &new_path) {
continue;
}
shifts.push(Shift {
edge_idx: source_seg.edge_idx,
new_path,
});
already_shifted.insert(source_seg.edge_idx);
}
}
shifts
}
fn build_shifted_path(
old_path: &[(usize, usize)],
seg: &Segment,
target_row: usize,
) -> Vec<(usize, usize)> {
let (start_idx, end_idx) = seg.path_idx_range;
let mut new_path = Vec::with_capacity(old_path.len() + 4);
new_path.extend_from_slice(&old_path[..start_idx]);
let start_c = old_path[start_idx].0;
if let Some(&(prev_c, prev_r)) = new_path.last() {
if prev_c == start_c {
extend_vertical(&mut new_path, prev_c, prev_r, target_row);
} else {
extend_vertical(&mut new_path, start_c, prev_r, target_row);
}
} else {
new_path.push((start_c, target_row));
}
for &(c, _) in old_path.iter().take(end_idx + 1).skip(start_idx) {
let last = new_path.last().copied();
if last != Some((c, target_row)) {
new_path.push((c, target_row));
}
}
if end_idx + 1 < old_path.len() {
let end_c = old_path[end_idx].0;
let (next_c, next_r) = old_path[end_idx + 1];
if next_c == end_c {
extend_vertical(&mut new_path, end_c, target_row, next_r);
} else {
extend_vertical(&mut new_path, end_c, target_row, next_r);
let mut c = end_c;
while c != next_c {
if c < next_c {
c += 1;
} else {
c -= 1;
}
let cell = (c, next_r);
if new_path.last().copied() != Some(cell) {
new_path.push(cell);
}
}
}
for &cell in old_path.iter().skip(end_idx + 2) {
if new_path.last().copied() != Some(cell) {
new_path.push(cell);
}
}
}
new_path
}
fn extend_vertical(
path: &mut Vec<(usize, usize)>,
col: usize,
from_row: usize,
to_row: usize,
) {
if from_row == to_row {
let cell = (col, to_row);
if path.last().copied() != Some(cell) {
path.push(cell);
}
return;
}
if from_row < to_row {
for r in (from_row + 1)..=to_row {
let cell = (col, r);
if path.last().copied() != Some(cell) {
path.push(cell);
}
}
} else {
for r in (to_row..from_row).rev() {
let cell = (col, r);
if path.last().copied() != Some(cell) {
path.push(cell);
}
}
}
}
fn path_is_feasible(grid: &Grid, path: &[(usize, usize)]) -> bool {
if path.is_empty() {
return false;
}
let last = path.len() - 1;
for (i, &(c, r)) in path.iter().enumerate() {
if i == last {
continue;
}
if grid.is_node_box(c, r) {
return false;
}
}
true
}
fn apply_shifts(
grid: &mut Grid,
paths: &mut [Option<Vec<(usize, usize)>>],
shifts: Vec<Shift>,
tip_for: &impl Fn(usize) -> char,
) {
for shift in shifts {
let Some(old_path) = paths[shift.edge_idx].clone() else {
continue;
};
grid.erase_path(&old_path);
let tip = tip_for(shift.edge_idx);
if let Some(drawn) = grid.draw_path(shift.new_path.clone(), tip) {
paths[shift.edge_idx] = Some(drawn);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_path(cells: &[(usize, usize)]) -> Vec<(usize, usize)> {
cells.to_vec()
}
#[test]
fn collect_segments_splits_at_corners() {
let path = make_path(&[(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3)]);
let paths = vec![Some(path)];
let edge_is_back = vec![true];
let segs = collect_segments(&paths, &edge_is_back);
assert_eq!(segs.len(), 3);
assert_eq!(segs[0].axis, Axis::Vertical);
assert_eq!(segs[1].axis, Axis::Horizontal);
assert_eq!(segs[2].axis, Axis::Vertical);
}
#[test]
fn collect_segments_skips_forward_edges() {
let path = make_path(&[(0, 0), (0, 1), (0, 2), (1, 2)]);
let paths = vec![Some(path)];
let edge_is_back = vec![false];
let segs = collect_segments(&paths, &edge_is_back);
assert!(segs.is_empty());
}
#[test]
fn extend_vertical_descends() {
let mut p = vec![(5, 2)];
extend_vertical(&mut p, 5, 2, 5);
assert_eq!(p, vec![(5, 2), (5, 3), (5, 4), (5, 5)]);
}
#[test]
fn extend_vertical_ascends() {
let mut p = vec![(5, 5)];
extend_vertical(&mut p, 5, 5, 2);
assert_eq!(p, vec![(5, 5), (5, 4), (5, 3), (5, 2)]);
}
#[test]
fn build_shifted_path_moves_corridor_down_one_row() {
let old_path = vec![
(0, 0),
(0, 1),
(0, 2),
(0, 3),
(1, 3),
(2, 3),
(3, 3),
(4, 3),
(4, 2),
(4, 1),
(4, 0),
];
let seg = Segment {
edge_idx: 0,
axis: Axis::Horizontal,
fixed_coord: 3,
range: (0, 4),
path_idx_range: (3, 7),
};
let new_path = build_shifted_path(&old_path, &seg, 4);
assert!(new_path.contains(&(0, 4)));
assert!(new_path.contains(&(4, 4)));
for w in new_path.windows(2) {
let (a, b) = (w[0], w[1]);
let dc = a.0.abs_diff(b.0);
let dr = a.1.abs_diff(b.1);
assert!(
dc + dr == 1,
"non-orthogonal step from {a:?} to {b:?} in {new_path:?}"
);
}
}
}