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 {
match Spine::solve(art, 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 = art.width();
let h = art.height();
let mut map = RankMap::new(w, h);
let Some(spine) = Spine::solve(art, self.start, self.bridge) else {
return map; };
let diameter = spine.diameter as f32;
let norm = |d: u32| {
if diameter > 0.0 {
d as f32 / diameter
} else {
0.0
}
};
let cells = w as usize * h as usize;
let mut inherited: Vec<Option<f32>> = vec![None; cells];
let mut queue = VecDeque::new();
for (i, dist) in spine.dist.iter().enumerate() {
if let Some(d) = dist {
inherited[i] = Some(norm(*d));
queue.push_back(i);
}
}
while let Some(cur) = queue.pop_front() {
let rank = inherited[cur];
for ni in neighbours(cur, w, h) {
if inherited[ni].is_none() {
inherited[ni] = rank;
queue.push_back(ni);
}
}
}
for cell in art.ink_cells() {
let rank = inherited[art.index(cell.x, cell.y)].unwrap_or(0.0);
map.set(cell.x, cell.y, rank);
}
map
}
}
const STRICT_CONNECTED_MIN: f32 = 0.6;
struct Spine {
dist: Vec<Option<u32>>,
diameter: u32,
}
impl Spine {
fn solve(art: &Art, hint: StartHint, bridge: u16) -> Option<Self> {
let strict_seed = largest_component_seed(art, 0)?;
let strict_size = bfs(art, strict_seed, 0)
.0
.iter()
.filter(|d| d.is_some())
.count();
let bridge = if (strict_size as f32) < STRICT_CONNECTED_MIN * art.ink_count().max(1) as f32
{
bridge
} else {
0
};
let seed = if bridge == 0 {
strict_seed
} else {
largest_component_seed(art, bridge)?
};
let (_, far_a) = bfs(art, seed, bridge);
let (dist_a, far_b) = bfs(art, far_a, bridge);
let (dist_b, _) = bfs(art, far_b, bridge);
let w = art.width() as usize;
let coord = |i: usize| ((i % w) as u16, (i / w) 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 largest_component_seed(art: &Art, bridge: u16) -> Option<usize> {
let (w, h) = (art.width(), art.height());
let mut visited = vec![false; w as usize * h as usize];
let mut queue = VecDeque::new();
let mut best: Option<(usize, usize)> = None;
for cell in art.ink_cells() {
let seed = art.index(cell.x, cell.y);
if 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(art, 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(art: &Art, source: usize, bridge: u16) -> (Vec<Option<u32>>, usize) {
let (w, h) = (art.width(), art.height());
let mut dist = vec![None; w as usize * h as usize];
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(art, cur, bridge) {
if dist[ni].is_none() {
dist[ni] = Some(d + 1);
queue.push_back(ni);
}
}
}
(dist, farthest)
}
fn bridged_neighbours(art: &Art, index: usize, bridge: u16) -> Vec<usize> {
let (w, h) = (art.width() as i32, art.height() as i32);
let r = bridge as i32 + 1;
let (cx, cy) = (index as i32 % w, index as i32 / w);
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 < w && ny < h {
let ni = (ny * w + nx) as usize;
if is_ink_index(art, ni) {
out.push(ni);
}
}
}
}
out
}
#[inline]
fn is_ink_index(art: &Art, index: usize) -> bool {
let w = art.width() as usize;
art.is_ink((index % w) as u16, (index / w) as u16)
}
#[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 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"
);
}
}