#[derive(Debug, Clone, Copy)]
pub struct LineInfo {
pub y_start: usize,
pub y_end: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct CharSeg {
pub x: usize,
pub width: usize,
}
pub fn horizontal_projection(grid: &[Vec<bool>], height: usize, width: usize) -> Vec<usize> {
let mut proj = vec![0; height];
for y in 0..height {
for x in 0..width {
if grid[y][x] {
proj[y] += 1;
}
}
}
proj
}
fn find_lines_from_projection(proj: &[usize], height: usize) -> Vec<LineInfo> {
let mut lines = Vec::new();
let mut in_line = false;
let mut start = 0;
let min_height = 2;
for (y, &count) in proj.iter().enumerate() {
if count > 0 && !in_line {
start = y;
in_line = true;
} else if count == 0 && in_line {
if y.saturating_sub(start) >= min_height {
lines.push(LineInfo { y_start: start, y_end: y });
}
in_line = false;
}
}
if in_line && height.saturating_sub(start) >= min_height {
lines.push(LineInfo { y_start: start, y_end: height });
}
lines
}
fn merge_fragmented_lines(lines: Vec<LineInfo>, height: usize) -> Vec<LineInfo> {
if lines.len() < 2 {
return lines;
}
let max_gap = (height / 8).min(20);
if max_gap < 2 {
return lines;
}
let mut out = vec![lines[0]];
for next in lines.into_iter().skip(1) {
let gap = next.y_start.saturating_sub(out.last().unwrap().y_end);
if gap <= max_gap {
out.last_mut().unwrap().y_end = next.y_end;
} else {
out.push(next);
}
}
out
}
pub fn vertical_projection(
grid: &[Vec<bool>],
y_start: usize,
y_end: usize,
width: usize,
) -> Vec<usize> {
let mut proj = vec![0; width];
for y in y_start..y_end {
for x in 0..width {
if grid[y][x] {
proj[x] += 1;
}
}
}
proj
}
pub fn find_lines(grid: &[Vec<bool>], width: usize, height: usize) -> Vec<LineInfo> {
let proj = horizontal_projection(grid, height, width);
let lines = find_lines_from_projection(&proj, height);
merge_fragmented_lines(lines, height)
}
pub fn find_chars(grid: &[Vec<bool>], line: &LineInfo, width: usize) -> Vec<CharSeg> {
let proj = vertical_projection(grid, line.y_start, line.y_end, width);
let mut chars = Vec::new();
let mut in_char = false;
let mut start = 0;
let min_width = 2;
for (x, &count) in proj.iter().enumerate() {
if count > 0 && !in_char {
start = x;
in_char = true;
} else if count == 0 && in_char {
if x.saturating_sub(start) >= min_width {
chars.push(CharSeg { x: start, width: x - start });
}
in_char = false;
}
}
if in_char && width.saturating_sub(start) >= min_width {
chars.push(CharSeg { x: start, width: width - start });
}
chars
}
pub fn compute_gaps(chars: &[CharSeg]) -> Vec<usize> {
chars.windows(2).map(|w| {
w[1].x.saturating_sub(w[0].x + w[0].width)
}).collect()
}
pub fn average_gap(gaps: &[usize]) -> f64 {
if gaps.is_empty() { return 0.0; }
gaps.iter().sum::<usize>() as f64 / gaps.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
fn make_grid(patterns: &[&str]) -> Vec<Vec<bool>> {
patterns.iter().map(|s| s.chars().map(|c| c == '#').collect()).collect()
}
#[test]
fn test_find_lines_single() {
let grid = make_grid(&[
".....",
".##..",
".##..",
".....",
".....",
]);
let lines = find_lines(&grid, 5, 5);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].y_start, 1);
assert_eq!(lines[0].y_end, 3);
}
#[test]
fn test_find_lines_two() {
let grid = make_grid(&[
".##..",
".##..",
".....",
".##..",
".##..",
]);
let lines = find_lines(&grid, 5, 5);
assert_eq!(lines.len(), 2);
}
#[test]
fn test_find_chars_two() {
let grid = make_grid(&[
".##.",
".##.",
"....",
"..#.",
"..#.",
]);
let line = LineInfo { y_start: 0, y_end: 2 };
let chars = find_chars(&grid, &line, 4);
assert_eq!(chars.len(), 1);
assert_eq!(chars[0].x, 1);
assert_eq!(chars[0].width, 2);
}
#[test]
fn test_compute_gaps() {
let chars = vec![
CharSeg { x: 2, width: 3 },
CharSeg { x: 8, width: 2 },
CharSeg { x: 14, width: 3 },
];
let gaps = compute_gaps(&chars);
assert_eq!(gaps, vec![3, 4]);
}
#[test]
fn test_horizontal_projection() {
let grid = make_grid(&[
".#.#.",
"..#..",
]);
let proj = horizontal_projection(&grid, 2, 5);
assert_eq!(proj[0], 2);
assert_eq!(proj[1], 1);
}
#[test]
fn test_find_lines_empty() {
let grid = make_grid(&[".....", ".....", "....."]);
let lines = find_lines(&grid, 5, 3);
assert!(lines.is_empty());
}
}