use std::collections::VecDeque;
use crate::{art::Art, rank::RankMap};
pub trait Ordering {
fn rank(&self, art: &Art) -> RankMap;
}
#[inline]
fn even_step(count: usize) -> f32 {
count.saturating_sub(1).max(1) as f32
}
#[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 denom = even_step(art.ink_count());
for (i, cell) in art.ink_cells().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 ltr() -> Self {
Directional(Direction::LeftToRight)
}
pub fn rtl() -> Self {
Directional(Direction::RightToLeft)
}
pub fn reading() -> Self {
if locale_is_rtl() {
Self::rtl()
} else {
Self::ltr()
}
}
}
const RTL_LANGS: [&str; 4] = ["ar", "he", "fa", "ur"];
fn locale_is_rtl() -> bool {
let tagged = |l: &str| {
let l = l.to_ascii_lowercase();
RTL_LANGS.iter().any(|p| l.starts_with(p))
};
if let Ok(l) = std::env::var("LC_ALL").or_else(|_| std::env::var("LANG")) {
return tagged(&l);
}
system_locale().map(|l| tagged(&l)).unwrap_or(false)
}
#[cfg(windows)]
fn system_locale() -> Option<String> {
#[link(name = "kernel32")]
extern "system" {
fn GetUserDefaultLocaleName(name: *mut u16, capacity: i32) -> i32;
}
let mut buf = [0u16; 85];
let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) };
if len <= 1 {
return None; }
String::from_utf16(&buf[..len as usize - 1]).ok()
}
#[cfg(not(windows))]
fn system_locale() -> Option<String> {
None
}
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 is_wide(w, h) => Direction::LeftToRight,
Direction::Auto => Direction::TopToBottom,
other => other,
};
let dx = even_step(w as usize);
let dy = even_step(h as usize);
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
}
}
#[inline]
fn is_wide(w: u16, h: u16) -> bool {
w as u32 > 2 * h as u32
}
#[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, PartialEq, Eq)]
pub struct GeodesicReport {
pub ink_cells: usize,
pub connected_cells: usize,
pub skeleton_cells: usize,
pub pieces: usize,
pub spine_length: u32,
}
impl Geodesic {
pub fn diagnose(&self, art: &Art) -> GeodesicReport {
let (w, h) = (art.width(), art.height());
let ink = ink_mask(art);
let ink_cells = ink.iter().filter(|&&m| m).count();
if ink_cells == 0 {
return GeodesicReport {
ink_cells: 0,
connected_cells: 0,
skeleton_cells: 0,
pieces: 0,
spine_length: 0,
};
}
let connected_cells = largest_component(&ink, w, h, 0).map_or(0, |(size, _)| size);
let skel = skeletonize(art);
let skeleton_cells = skel.iter().filter(|&&m| m).count();
let bridge = adaptive_bridge(&skel, w, h, self.bridge);
GeodesicReport {
ink_cells,
connected_cells,
skeleton_cells,
pieces: components(&skel, w, h, bridge).len(),
spine_length: spine(&skel, w, h, self.start, self.bridge)
.map_or(0, |trace| trace.diameter),
}
}
}
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;
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 = even_step(order.len());
for (i, &(x, y, _, _)) in order.iter().enumerate() {
map.set(x, y, i as f32 / denom);
}
map
}
}
pub const STRICT_CONNECTED_MIN: f32 = 0.6;
struct Trace {
dist: Vec<Option<u32>>,
diameter: u32,
}
fn trace(mask: &[bool], w: u16, h: u16, seed: usize, hint: StartHint, bridge: u16) -> Trace {
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);
Trace { dist, diameter }
}
fn spine(mask: &[bool], w: u16, h: u16, hint: StartHint, bridge: u16) -> Option<Trace> {
let bridge = adaptive_bridge(mask, w, h, bridge);
let (_, seed) = largest_component(mask, w, h, bridge)?;
Some(trace(mask, w, h, seed, hint, bridge))
}
fn adaptive_bridge(mask: &[bool], w: u16, h: u16, bridge: u16) -> u16 {
if bridge == 0 {
return 0;
}
let count = mask.iter().filter(|&&m| m).count();
match largest_component(mask, w, h, 0) {
Some((strict, _)) if strict as f32 >= STRICT_CONNECTED_MIN * count.max(1) as f32 => 0,
_ => bridge,
}
}
fn neighbours(index: usize, w: u16, h: u16) -> impl Iterator<Item = usize> {
offsets(index, w, h, 0)
}
fn offsets(index: usize, w: u16, h: u16, bridge: u16) -> impl Iterator<Item = usize> {
let (wi, hi) = (w as i32, h as i32);
let r = bridge as i32 + 1;
let (cx, cy) = (index as i32 % wi.max(1), index as i32 / wi.max(1));
(-r..=r)
.flat_map(move |dy| (-r..=r).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 < wi && ny < hi).then_some((ny * wi + nx) as usize)
})
}
#[inline]
fn bridged_neighbours(
mask: &[bool],
w: u16,
h: u16,
index: usize,
bridge: u16,
) -> impl Iterator<Item = usize> + '_ {
offsets(index, w, h, bridge).filter(move |&ni| mask[ni])
}
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.max(1)) as u16, (i / w.max(1)) as u16))
.collect()
}
fn components(mask: &[bool], w: u16, h: u16, bridge: u16) -> Vec<Vec<usize>> {
let mut seen = vec![false; mask.len()];
let mut queue = VecDeque::new();
let mut out = Vec::new();
for seed in 0..mask.len() {
if !mask[seed] || seen[seed] {
continue;
}
let mut cells = Vec::new();
seen[seed] = true;
queue.push_back(seed);
while let Some(cur) = queue.pop_front() {
cells.push(cur);
for ni in bridged_neighbours(mask, w, h, cur, bridge) {
if !seen[ni] {
seen[ni] = true;
queue.push_back(ni);
}
}
}
out.push(cells);
}
out
}
fn largest_component(mask: &[bool], w: u16, h: u16, bridge: u16) -> Option<(usize, usize)> {
components(mask, w, h, bridge)
.into_iter()
.map(|c| (c.len(), c[0]))
.max_by_key(|&(size, _)| size)
}
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 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()];
if !skel.iter().any(|&m| m) {
return value;
}
let bridge = adaptive_bridge(skel, w, h, bridge);
let horizontal = is_wide(w, h);
let axis = |i: usize| -> u16 {
if horizontal {
(i % w as usize) as u16
} else {
(i / w as usize) as u16
}
};
let mut pieces: Vec<(u16, Vec<usize>, Trace)> = components(skel, w, h, bridge)
.into_iter()
.map(|comp| {
let lead = comp.iter().map(|&c| axis(c)).min().unwrap_or(0);
let traced = trace(skel, w, h, comp[0], hint, bridge);
(lead, comp, traced)
})
.collect();
pieces.sort_by_key(|(lead, _, _)| *lead);
for (index, (_, comp, traced)) in pieces.iter().enumerate() {
let span = traced.diameter.max(1) as f32;
for &cell in comp {
let within = traced.dist[cell].map_or(0.0, |d| d as f32 / span);
value[cell] = index as f32 + within;
}
}
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 diagnose_reports_the_traced_skeleton() {
let art = Art::parse(&"##########\n".repeat(3));
let report = Geodesic::default().diagnose(&art);
assert_eq!(report.ink_cells, 30);
assert_eq!(report.connected_cells, 30);
assert!(
report.skeleton_cells < report.ink_cells,
"thinning should shrink the ink: {report:?}"
);
assert_eq!(report.pieces, 1);
assert!(
(report.spine_length as usize) < report.ink_cells,
"spine must be the centerline, not the ink: {report:?}"
);
}
#[test]
fn diagnose_counts_pieces() {
let art = Art::parse("## ## ##");
let report = Geodesic::default().diagnose(&art);
assert_eq!(report.pieces, 3);
}
#[test]
fn diagnose_of_empty_art_is_all_zero() {
let report = Geodesic::default().diagnose(&Art::parse(" \n "));
assert_eq!(report.ink_cells, 0);
assert_eq!(report.spine_length, 0);
assert_eq!(report.pieces, 0);
}
#[test]
fn bridges_small_gaps_when_fragmented() {
let art = Art::parse("== ==");
let strict = Geodesic {
start: StartHint::TopLeft,
bridge: 0,
};
assert_eq!(strict.diagnose(&art).pieces, 2);
assert_eq!(Geodesic::default().diagnose(&art).pieces, 1);
}
#[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_eq!(report.pieces, 1);
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"
);
}
#[test]
fn padding_does_not_steer_auto() {
let padded = Directional(Direction::Auto).rank(&Art::parse(" #\n #"));
let bare = Directional(Direction::Auto).rank(&Art::parse("#\n#"));
assert_eq!(padded.rank_at(0, 0), bare.rank_at(0, 0));
assert_eq!(padded.rank_at(0, 0), Some(0.0));
assert_eq!(padded.rank_at(0, 1), Some(1.0));
}
#[test]
fn explicit_direction_beats_locale_sniffing() {
let art = Art::parse("abcd");
let ltr = Directional::ltr().rank(&art);
let rtl = Directional::rtl().rank(&art);
assert_eq!(ltr.rank_at(0, 0), Some(0.0));
assert_eq!(rtl.rank_at(3, 0), Some(0.0));
}
#[test]
fn scanline_spans_the_whole_bar() {
let art = Art::parse("ab\ncd");
let r = Scanline.rank(&art);
assert_eq!(r.rank_at(0, 0), Some(0.0));
assert_eq!(r.rank_at(1, 1), Some(1.0));
}
fn revealed_share(art: &Art, ranks: &RankMap, progress: f32) -> f32 {
let mut seen = 0usize;
for y in 0..art.height() {
for x in 0..art.width() {
if art.is_ink(x, y) && ranks.visible_at(x, y, progress) {
seen += 1;
}
}
}
seen as f32 / art.ink_count().max(1) as f32
}
#[test]
fn revealed_share_tracks_progress_on_the_bundled_art() {
let art = [
("dragon", Art::parse(include_str!("../assets/dragon.txt"))),
("serpent", Art::parse(include_str!("../assets/serpent.txt"))),
("inkling", Art::parse(include_str!("../assets/inkling.txt"))),
];
for (name, art) in &art {
let maps: [(&str, RankMap, f32); 3] = [
("directional", Directional::default().rank(art), 0.2),
("geodesic", Geodesic::default().rank(art), 0.02),
("scanline", Scanline.rank(art), 0.02),
];
for (ordering, ranks, tolerance) in maps {
let at = |p| revealed_share(art, &ranks, p);
assert!(
at(0.0) < 0.02,
"{name}/{ordering}: {:.0}% of the ink is already showing at zero",
at(0.0) * 100.0
);
for p in [0.25f32, 0.5, 0.75] {
let share = at(p);
assert!(
(share - p).abs() < tolerance,
"{name}/{ordering}: {:.0}% of the ink revealed at {:.0}% progress",
share * 100.0,
p * 100.0
);
}
assert!(
at(1.0) > 0.999,
"{name}/{ordering}: the art never finishes filling"
);
}
}
}
#[test]
fn orderings_tolerate_empty_art() {
let art = Art::parse("");
for map in [
Scanline.rank(&art),
Directional::default().rank(&art),
Geodesic::default().rank(&art),
] {
assert_eq!(map.ink_count(), 0);
}
}
}