use std::collections::VecDeque;
use crate::{art::Art, rank::RankMap};
pub trait Ordering {
fn rank(&self, art: &Art) -> RankMap;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Scanline;
impl Ordering for Scanline {
fn rank(&self, art: &Art) -> RankMap {
let mut map = RankMap::new(art.width(), art.height());
let cells: Vec<_> = art.ink_cells().collect();
let denom = cells.len().saturating_sub(1).max(1) as f32;
for (i, cell) in cells.iter().enumerate() {
map.set(cell.x, cell.y, i as f32 / denom);
}
map
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Direction {
#[default]
TopToBottom,
BottomToTop,
LeftToRight,
RightToLeft,
Auto,
}
#[derive(Clone, Copy, Debug)]
pub struct Directional(pub Direction);
impl Default for Directional {
fn default() -> Self {
Directional(Direction::Auto)
}
}
impl Directional {
pub fn reading() -> Self {
let rtl = std::env::var("LC_ALL")
.or_else(|_| std::env::var("LANG"))
.map(|l| {
let l = l.to_ascii_lowercase();
["ar", "he", "fa", "ur"].iter().any(|p| l.starts_with(p))
})
.unwrap_or(false);
Directional(if rtl {
Direction::RightToLeft
} else {
Direction::LeftToRight
})
}
}
impl Ordering for Directional {
fn rank(&self, art: &Art) -> RankMap {
let (w, h) = (art.width(), art.height());
let dir = match self.0 {
Direction::Auto if w as u32 > 2 * h as u32 => Direction::LeftToRight,
Direction::Auto => Direction::TopToBottom,
other => other,
};
let dx = w.saturating_sub(1).max(1) as f32;
let dy = h.saturating_sub(1).max(1) as f32;
let mut map = RankMap::new(w, h);
for cell in art.ink_cells() {
let rank = match dir {
Direction::BottomToTop => (h - 1 - cell.y) as f32 / dy,
Direction::LeftToRight => cell.x as f32 / dx,
Direction::RightToLeft => (w - 1 - cell.x) as f32 / dx,
_ => cell.y as f32 / dy, };
map.set(cell.x, cell.y, rank);
}
map
}
}
#[derive(Clone, Copy, Debug)]
pub struct Geodesic {
pub start: StartHint,
pub bridge: u16,
}
impl Default for Geodesic {
fn default() -> Self {
Geodesic {
start: StartHint::default(),
bridge: 1,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StartHint {
#[default]
TopLeft,
Bottom,
Topological,
}
#[derive(Clone, Copy, Debug)]
pub struct GeodesicReport {
pub ink_cells: usize,
pub connected_cells: usize,
pub spine_length: u32,
}
impl Geodesic {
pub fn diagnose(&self, art: &Art) -> GeodesicReport {
let mask = ink_mask(art);
match Spine::solve(&mask, art.width(), art.height(), self.start, self.bridge) {
Some(spine) => GeodesicReport {
ink_cells: art.ink_count(),
connected_cells: spine.dist.iter().filter(|d| d.is_some()).count(),
spine_length: spine.diameter,
},
None => GeodesicReport {
ink_cells: 0,
connected_cells: 0,
spine_length: 0,
},
}
}
}
impl Ordering for Geodesic {
fn rank(&self, art: &Art) -> RankMap {
let (w, h) = (art.width(), art.height());
let mut map = RankMap::new(w, h);
if art.ink_count() == 0 {
return map;
}
let skel = skeletonize(art);
let value = skeleton_values(&skel, w, h, self.start, self.bridge);
let mut val = value.clone();
let mut depth = vec![0u32; val.len()];
let mut queue: VecDeque<usize> = (0..val.len()).filter(|&i| !val[i].is_nan()).collect();
while let Some(cur) = queue.pop_front() {
for ni in neighbours(cur, w, h) {
if val[ni].is_nan() {
val[ni] = val[cur];
depth[ni] = depth[cur] + 1;
queue.push_back(ni);
}
}
}
let mut order: Vec<(u16, u16, f32, u32)> = art
.ink_cells()
.map(|c| {
let i = art.index(c.x, c.y);
(c.x, c.y, val[i], depth[i])
})
.collect();
order.sort_by(|a, b| a.2.total_cmp(&b.2).then(a.3.cmp(&b.3)));
let denom = order.len().saturating_sub(1).max(1) as f32;
for (i, &(x, y, _, _)) in order.iter().enumerate() {
map.set(x, y, i as f32 / denom);
}
map
}
}
const STRICT_CONNECTED_MIN: f32 = 0.6;
struct Spine {
dist: Vec<Option<u32>>,
diameter: u32,
}
impl Spine {
fn solve(mask: &[bool], w: u16, h: u16, hint: StartHint, bridge: u16) -> Option<Self> {
let count = mask.iter().filter(|&&m| m).count();
let strict_seed = largest_component_seed(mask, w, h, 0)?;
let strict_size = bfs(mask, w, h, strict_seed, 0)
.0
.iter()
.filter(|d| d.is_some())
.count();
let bridge = if (strict_size as f32) < STRICT_CONNECTED_MIN * count.max(1) as f32 {
bridge
} else {
0
};
let seed = if bridge == 0 {
strict_seed
} else {
largest_component_seed(mask, w, h, bridge)?
};
let (_, far_a) = bfs(mask, w, h, seed, bridge);
let (dist_a, far_b) = bfs(mask, w, h, far_a, bridge);
let (dist_b, _) = bfs(mask, w, h, far_b, bridge);
let coord = |i: usize| ((i % w as usize) as u16, (i / w as usize) as u16);
let (ax, ay) = coord(far_a);
let (bx, by) = coord(far_b);
let start_is_a = match hint {
StartHint::Topological => true,
StartHint::TopLeft => (ay, ax) <= (by, bx),
StartHint::Bottom => ay >= by,
};
let dist = if start_is_a { dist_a } else { dist_b };
let diameter = dist.iter().flatten().copied().max().unwrap_or(0);
Some(Spine { dist, diameter })
}
}
fn neighbours(index: usize, w: u16, h: u16) -> impl Iterator<Item = usize> {
let (w, h) = (w as i32, h as i32);
let (cx, cy) = ((index as i32 % w), (index as i32 / w));
(-1..=1)
.flat_map(move |dy| (-1..=1).map(move |dx| (dx, dy)))
.filter_map(move |(dx, dy)| {
if dx == 0 && dy == 0 {
return None;
}
let (nx, ny) = (cx + dx, cy + dy);
(nx >= 0 && ny >= 0 && nx < w && ny < h).then_some((ny * w + nx) as usize)
})
}
fn ink_mask(art: &Art) -> Vec<bool> {
let (w, h) = (art.width() as usize, art.height() as usize);
(0..w * h)
.map(|i| art.is_ink((i % w) as u16, (i / w) as u16))
.collect()
}
fn largest_component_seed(mask: &[bool], w: u16, h: u16, bridge: u16) -> Option<usize> {
let mut visited = vec![false; mask.len()];
let mut queue = VecDeque::new();
let mut best: Option<(usize, usize)> = None;
for seed in 0..mask.len() {
if !mask[seed] || visited[seed] {
continue;
}
let mut size = 0usize;
visited[seed] = true;
queue.push_back(seed);
while let Some(cur) = queue.pop_front() {
size += 1;
for ni in bridged_neighbours(mask, w, h, cur, bridge) {
if !visited[ni] {
visited[ni] = true;
queue.push_back(ni);
}
}
}
if best.map_or(true, |(best_size, _)| size > best_size) {
best = Some((size, seed));
}
}
best.map(|(_, seed)| seed)
}
fn bfs(mask: &[bool], w: u16, h: u16, source: usize, bridge: u16) -> (Vec<Option<u32>>, usize) {
let mut dist = vec![None; mask.len()];
let mut queue = VecDeque::new();
dist[source] = Some(0);
queue.push_back(source);
let (mut farthest, mut far_d) = (source, 0u32);
while let Some(cur) = queue.pop_front() {
let d = dist[cur].unwrap();
if d > far_d {
far_d = d;
farthest = cur;
}
for ni in bridged_neighbours(mask, w, h, cur, bridge) {
if dist[ni].is_none() {
dist[ni] = Some(d + 1);
queue.push_back(ni);
}
}
}
(dist, farthest)
}
fn bridged_neighbours(mask: &[bool], w: u16, h: u16, index: usize, bridge: u16) -> Vec<usize> {
let (wi, hi) = (w as i32, h as i32);
let r = bridge as i32 + 1;
let (cx, cy) = (index as i32 % wi, index as i32 / wi);
let mut out = Vec::new();
for dy in -r..=r {
for dx in -r..=r {
if dx == 0 && dy == 0 {
continue;
}
let (nx, ny) = (cx + dx, cy + dy);
if nx >= 0 && ny >= 0 && nx < wi && ny < hi {
let ni = (ny * wi + nx) as usize;
if mask[ni] {
out.push(ni);
}
}
}
}
out
}
fn skeletonize(art: &Art) -> Vec<bool> {
let (w, h) = (art.width() as i32, art.height() as i32);
let idx = |x: i32, y: i32| (y * w + x) as usize;
let mut g = ink_mask(art);
let val = |g: &[bool], x: i32, y: i32| -> u8 {
(x >= 0 && y >= 0 && x < w && y < h && g[idx(x, y)]) as u8
};
loop {
let mut removed = false;
for step in 0..2 {
let mut marks = Vec::new();
for y in 0..h {
for x in 0..w {
if !g[idx(x, y)] {
continue;
}
let p = [
val(&g, x, y - 1),
val(&g, x + 1, y - 1),
val(&g, x + 1, y),
val(&g, x + 1, y + 1),
val(&g, x, y + 1),
val(&g, x - 1, y + 1),
val(&g, x - 1, y),
val(&g, x - 1, y - 1),
];
let b: u8 = p.iter().sum();
if !(2..=6).contains(&b) {
continue;
}
let a = (0..8).filter(|&i| p[i] == 0 && p[(i + 1) % 8] == 1).count();
if a != 1 {
continue;
}
let (c1, c2) = if step == 0 {
(p[0] * p[2] * p[4], p[2] * p[4] * p[6])
} else {
(p[0] * p[2] * p[6], p[0] * p[4] * p[6])
};
if c1 == 0 && c2 == 0 {
marks.push(idx(x, y));
}
}
}
if !marks.is_empty() {
removed = true;
for i in marks {
g[i] = false;
}
}
}
if !removed {
break;
}
}
g
}
fn skeleton_values(skel: &[bool], w: u16, h: u16, hint: StartHint, bridge: u16) -> Vec<f32> {
let mut value = vec![f32::NAN; skel.len()];
let count = skel.iter().filter(|&&m| m).count();
if count == 0 {
return value;
}
let bridge = match largest_component_seed(skel, w, h, 0) {
Some(seed)
if bfs(skel, w, h, seed, 0)
.0
.iter()
.filter(|d| d.is_some())
.count() as f32
>= STRICT_CONNECTED_MIN * count as f32 =>
{
0
}
_ => bridge,
};
let mut comp_id = vec![usize::MAX; skel.len()];
let mut comps: Vec<Vec<usize>> = Vec::new();
for i in 0..skel.len() {
if !skel[i] || comp_id[i] != usize::MAX {
continue;
}
let id = comps.len();
let mut cells = Vec::new();
let mut queue = VecDeque::new();
comp_id[i] = id;
queue.push_back(i);
while let Some(cur) = queue.pop_front() {
cells.push(cur);
for ni in bridged_neighbours(skel, w, h, cur, bridge) {
if comp_id[ni] == usize::MAX {
comp_id[ni] = id;
queue.push_back(ni);
}
}
}
comps.push(cells);
}
let horizontal = w as u32 > 2 * h as u32;
let axis = |i: usize| -> u16 {
if horizontal {
(i % w as usize) as u16
} else {
(i / w as usize) as u16
}
};
let coord = |i: usize| ((i % w as usize) as u16, (i / w as usize) as u16);
let mut pieces: Vec<(u16, Vec<(usize, f32)>)> = comps
.iter()
.map(|comp| {
let (_, far_a) = bfs(skel, w, h, comp[0], bridge);
let (dist_a, far_b) = bfs(skel, w, h, far_a, bridge);
let (dist_b, _) = bfs(skel, w, h, far_b, bridge);
let (ax, ay) = coord(far_a);
let (bx, by) = coord(far_b);
let start_is_a = match hint {
StartHint::Topological => true,
StartHint::TopLeft => (ay, ax) <= (by, bx),
StartHint::Bottom => ay >= by,
};
let dist = if start_is_a { dist_a } else { dist_b };
let diameter = dist.iter().flatten().copied().max().unwrap_or(0).max(1) as f32;
let lead = comp.iter().map(|&c| axis(c)).min().unwrap_or(0);
let within = comp
.iter()
.map(|&c| (c, dist[c].map_or(0.0, |d| d as f32 / diameter)))
.collect();
(lead, within)
})
.collect();
pieces.sort_by_key(|(lead, _)| *lead);
for (piece, (_, within)) in pieces.iter().enumerate() {
for &(cell, w) in within {
value[cell] = piece as f32 + w;
}
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn straight_line_reveals_along_itself() {
let art = Art::parse("=========");
let ranks = Geodesic::default().rank(&art);
let row: Vec<f32> = (0..art.width())
.map(|x| ranks.rank_at(x, 0).unwrap())
.collect();
let increasing = row.windows(2).all(|w| w[0] <= w[1]);
let decreasing = row.windows(2).all(|w| w[0] >= w[1]);
assert!(
increasing || decreasing,
"spine reveal was not monotone: {row:?}"
);
assert!((row.iter().cloned().fold(0.0_f32, f32::max) - 1.0).abs() < 1e-6);
}
#[test]
fn spine_traces_largest_component() {
let art = Art::parse(".\n\n ========");
let report = Geodesic::default().diagnose(&art);
assert_eq!(report.ink_cells, 9);
assert_eq!(report.connected_cells, 8); }
#[test]
fn islands_inherit_nearest_spine_rank() {
let art = Art::parse(". ====== .");
let ranks = Geodesic::default().rank(&art);
let left = ranks.rank_at(0, 0).unwrap();
let right = ranks.rank_at(11, 0).unwrap();
assert!(left < right, "left {left} should precede right {right}");
assert!(left < 0.25 && right > 0.75, "left={left} right={right}");
}
#[test]
fn diagnose_counts_connectivity() {
let report = Geodesic::default().diagnose(&Art::parse("========== ."));
assert_eq!(report.ink_cells, 11);
assert_eq!(report.connected_cells, 10); }
#[test]
fn bridges_small_gaps_when_fragmented() {
let art = Art::parse("== ==");
let strict = Geodesic {
start: StartHint::TopLeft,
bridge: 0,
};
assert_eq!(strict.diagnose(&art).connected_cells, 2);
assert_eq!(Geodesic::default().diagnose(&art).connected_cells, 4);
}
#[test]
fn connected_art_is_not_bridged() {
let art = Art::parse("####\n #\n####\n#\n####");
let report = Geodesic::default().diagnose(&art);
assert_eq!(report.connected_cells, report.ink_cells);
assert!(
report.spine_length >= 9,
"spine was {}",
report.spine_length
);
}
#[test]
fn solid_block_reveals_across_the_whole_bar() {
let art = Art::parse(&"########\n".repeat(8));
let r = Geodesic::default().rank(&art);
let ranks: Vec<f32> = (0..8)
.flat_map(|y| (0..8u16).map(move |x| (x, y)))
.map(|(x, y)| r.rank_at(x, y).unwrap())
.collect();
let lo = ranks.iter().cloned().fold(f32::MAX, f32::min);
let hi = ranks.iter().cloned().fold(f32::MIN, f32::max);
assert!(
lo < 0.02 && hi > 0.98,
"block did not use the whole bar: {lo}..{hi}"
);
}
#[test]
fn separate_pieces_reveal_in_reading_order() {
let art = Art::parse("## ##\n## ##\n## ##");
let r = Geodesic::default().rank(&art);
let left = r.rank_at(0, 1).unwrap();
let right = r.rank_at(11, 1).unwrap();
assert!(
left < right,
"left piece {left} should precede right {right}"
);
assert!(
left < 0.5 && right > 0.5,
"pieces out of order: {left} {right}"
);
}
#[test]
fn thin_line_stays_a_trace() {
let art = Art::parse("==============");
let r = Geodesic::default().rank(&art);
let row: Vec<f32> = (0..art.width()).map(|x| r.rank_at(x, 0).unwrap()).collect();
let lo = row.iter().cloned().fold(f32::MAX, f32::min);
let hi = row.iter().cloned().fold(f32::MIN, f32::max);
assert!(
lo < 0.01 && hi > 0.99,
"line did not trace end to end: {row:?}"
);
}
#[test]
fn directional_auto_accounts_for_cell_aspect() {
let tall = Art::parse("#####\n#####\n#####\n#####");
let r = Directional(Direction::Auto).rank(&tall);
assert!(
r.rank_at(0, 0).unwrap() < r.rank_at(0, 3).unwrap(),
"top first"
);
assert_eq!(
r.rank_at(0, 0),
r.rank_at(4, 0),
"same row reveals together"
);
let wide = Art::parse("##########\n##########");
let rw = Directional(Direction::Auto).rank(&wide);
assert!(
rw.rank_at(0, 0).unwrap() < rw.rank_at(9, 0).unwrap(),
"left first"
);
assert_eq!(
rw.rank_at(0, 0),
rw.rank_at(0, 1),
"same column reveals together"
);
}
}