1use std::collections::VecDeque;
9
10use crate::{art::Art, rank::RankMap};
11
12pub trait Ordering {
14 fn rank(&self, art: &Art) -> RankMap;
15}
16
17#[derive(Clone, Copy, Debug, Default)]
26pub struct Scanline;
27
28impl Ordering for Scanline {
29 fn rank(&self, art: &Art) -> RankMap {
30 let mut map = RankMap::new(art.width(), art.height());
31 let cells: Vec<_> = art.ink_cells().collect();
32 let denom = cells.len().saturating_sub(1).max(1) as f32;
33 for (i, cell) in cells.iter().enumerate() {
34 map.set(cell.x, cell.y, i as f32 / denom);
35 }
36 map
37 }
38}
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub enum Direction {
47 #[default]
49 TopToBottom,
50 BottomToTop,
52 LeftToRight,
54 RightToLeft,
56 Auto,
58}
59
60#[derive(Clone, Copy, Debug)]
65pub struct Directional(pub Direction);
66
67impl Default for Directional {
68 fn default() -> Self {
70 Directional(Direction::Auto)
71 }
72}
73
74impl Directional {
75 pub fn reading() -> Self {
78 let rtl = std::env::var("LC_ALL")
79 .or_else(|_| std::env::var("LANG"))
80 .map(|l| {
81 let l = l.to_ascii_lowercase();
82 ["ar", "he", "fa", "ur"].iter().any(|p| l.starts_with(p))
83 })
84 .unwrap_or(false);
85 Directional(if rtl {
86 Direction::RightToLeft
87 } else {
88 Direction::LeftToRight
89 })
90 }
91}
92
93impl Ordering for Directional {
94 fn rank(&self, art: &Art) -> RankMap {
95 let (w, h) = (art.width(), art.height());
96 let dir = match self.0 {
101 Direction::Auto if w as u32 > 2 * h as u32 => Direction::LeftToRight,
102 Direction::Auto => Direction::TopToBottom,
103 other => other,
104 };
105 let dx = w.saturating_sub(1).max(1) as f32;
106 let dy = h.saturating_sub(1).max(1) as f32;
107 let mut map = RankMap::new(w, h);
108 for cell in art.ink_cells() {
109 let rank = match dir {
110 Direction::BottomToTop => (h - 1 - cell.y) as f32 / dy,
111 Direction::LeftToRight => cell.x as f32 / dx,
112 Direction::RightToLeft => (w - 1 - cell.x) as f32 / dx,
113 _ => cell.y as f32 / dy, };
115 map.set(cell.x, cell.y, rank);
116 }
117 map
118 }
119}
120
121#[derive(Clone, Copy, Debug, Default)]
140pub struct Geodesic {
141 pub start: StartHint,
143}
144
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum StartHint {
148 #[default]
150 TopLeft,
151 Bottom,
153 Topological,
155}
156
157#[derive(Clone, Copy, Debug)]
162pub struct GeodesicReport {
163 pub ink_cells: usize,
164 pub connected_cells: usize,
166 pub spine_length: u32,
168}
169
170impl Geodesic {
171 pub fn diagnose(&self, art: &Art) -> GeodesicReport {
173 match Spine::solve(art, self.start) {
174 Some(spine) => GeodesicReport {
175 ink_cells: art.ink_count(),
176 connected_cells: spine.dist.iter().filter(|d| d.is_some()).count(),
177 spine_length: spine.diameter,
178 },
179 None => GeodesicReport {
180 ink_cells: 0,
181 connected_cells: 0,
182 spine_length: 0,
183 },
184 }
185 }
186}
187
188impl Ordering for Geodesic {
189 fn rank(&self, art: &Art) -> RankMap {
190 let w = art.width();
191 let h = art.height();
192 let mut map = RankMap::new(w, h);
193
194 let Some(spine) = Spine::solve(art, self.start) else {
195 return map; };
197
198 let diameter = spine.diameter as f32;
199 let norm = |d: u32| {
200 if diameter > 0.0 {
201 d as f32 / diameter
202 } else {
203 0.0
204 }
205 };
206
207 let cells = w as usize * h as usize;
212 let mut inherited: Vec<Option<f32>> = vec![None; cells];
213 let mut queue = VecDeque::new();
214 for (i, dist) in spine.dist.iter().enumerate() {
215 if let Some(d) = dist {
216 inherited[i] = Some(norm(*d));
217 queue.push_back(i);
218 }
219 }
220 while let Some(cur) = queue.pop_front() {
221 let rank = inherited[cur];
222 for ni in neighbours(cur, w, h) {
223 if inherited[ni].is_none() {
224 inherited[ni] = rank;
225 queue.push_back(ni);
226 }
227 }
228 }
229
230 for cell in art.ink_cells() {
231 let rank = inherited[art.index(cell.x, cell.y)].unwrap_or(0.0);
232 map.set(cell.x, cell.y, rank);
233 }
234 map
235 }
236}
237
238struct Spine {
240 dist: Vec<Option<u32>>,
243 diameter: u32,
245}
246
247impl Spine {
248 fn solve(art: &Art, hint: StartHint) -> Option<Self> {
249 let seed = largest_component_seed(art)?;
250
251 let (_, far_a) = bfs(art, seed);
253 let (dist_a, far_b) = bfs(art, far_a);
254 let (dist_b, _) = bfs(art, far_b);
255
256 let w = art.width() as usize;
258 let coord = |i: usize| ((i % w) as u16, (i / w) as u16);
259 let (ax, ay) = coord(far_a);
260 let (bx, by) = coord(far_b);
261 let start_is_a = match hint {
262 StartHint::Topological => true,
263 StartHint::TopLeft => (ay, ax) <= (by, bx),
264 StartHint::Bottom => ay >= by,
265 };
266
267 let dist = if start_is_a { dist_a } else { dist_b };
268 let diameter = dist.iter().flatten().copied().max().unwrap_or(0);
269 Some(Spine { dist, diameter })
270 }
271}
272
273fn neighbours(index: usize, w: u16, h: u16) -> impl Iterator<Item = usize> {
279 let (w, h) = (w as i32, h as i32);
280 let (cx, cy) = ((index as i32 % w), (index as i32 / w));
281 (-1..=1)
282 .flat_map(move |dy| (-1..=1).map(move |dx| (dx, dy)))
283 .filter_map(move |(dx, dy)| {
284 if dx == 0 && dy == 0 {
285 return None;
286 }
287 let (nx, ny) = (cx + dx, cy + dy);
288 (nx >= 0 && ny >= 0 && nx < w && ny < h).then_some((ny * w + nx) as usize)
289 })
290}
291
292fn largest_component_seed(art: &Art) -> Option<usize> {
294 let (w, h) = (art.width(), art.height());
295 let mut visited = vec![false; w as usize * h as usize];
296 let mut queue = VecDeque::new();
297 let mut best: Option<(usize, usize)> = None; for cell in art.ink_cells() {
300 let seed = art.index(cell.x, cell.y);
301 if visited[seed] {
302 continue;
303 }
304 let mut size = 0usize;
305 visited[seed] = true;
306 queue.push_back(seed);
307 while let Some(cur) = queue.pop_front() {
308 size += 1;
309 for ni in neighbours(cur, w, h) {
310 if !visited[ni] && is_ink_index(art, ni) {
311 visited[ni] = true;
312 queue.push_back(ni);
313 }
314 }
315 }
316 if best.map_or(true, |(best_size, _)| size > best_size) {
317 best = Some((size, seed));
318 }
319 }
320 best.map(|(_, seed)| seed)
321}
322
323fn bfs(art: &Art, source: usize) -> (Vec<Option<u32>>, usize) {
326 let (w, h) = (art.width(), art.height());
327 let mut dist = vec![None; w as usize * h as usize];
328 let mut queue = VecDeque::new();
329
330 dist[source] = Some(0);
331 queue.push_back(source);
332 let (mut farthest, mut far_d) = (source, 0u32);
333
334 while let Some(cur) = queue.pop_front() {
335 let d = dist[cur].unwrap();
336 if d > far_d {
337 far_d = d;
338 farthest = cur;
339 }
340 for ni in neighbours(cur, w, h) {
341 if dist[ni].is_none() && is_ink_index(art, ni) {
342 dist[ni] = Some(d + 1);
343 queue.push_back(ni);
344 }
345 }
346 }
347 (dist, farthest)
348}
349
350#[inline]
351fn is_ink_index(art: &Art, index: usize) -> bool {
352 let w = art.width() as usize;
353 art.is_ink((index % w) as u16, (index / w) as u16)
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
363 fn straight_line_reveals_along_itself() {
364 let art = Art::parse("=========");
365 let ranks = Geodesic::default().rank(&art);
366 let row: Vec<f32> = (0..art.width())
367 .map(|x| ranks.rank_at(x, 0).unwrap())
368 .collect();
369 let increasing = row.windows(2).all(|w| w[0] <= w[1]);
370 let decreasing = row.windows(2).all(|w| w[0] >= w[1]);
371 assert!(
372 increasing || decreasing,
373 "spine reveal was not monotone: {row:?}"
374 );
375 assert!((row.iter().cloned().fold(0.0_f32, f32::max) - 1.0).abs() < 1e-6);
376 }
377
378 #[test]
380 fn spine_traces_largest_component() {
381 let art = Art::parse(".\n\n ========");
382 let report = Geodesic::default().diagnose(&art);
383 assert_eq!(report.ink_cells, 9);
384 assert_eq!(report.connected_cells, 8); }
386
387 #[test]
390 fn islands_inherit_nearest_spine_rank() {
391 let art = Art::parse(". ====== .");
392 let ranks = Geodesic::default().rank(&art);
393 let left = ranks.rank_at(0, 0).unwrap();
394 let right = ranks.rank_at(11, 0).unwrap();
395 assert!(left < right, "left {left} should precede right {right}");
396 assert!(left < 0.25 && right > 0.75, "left={left} right={right}");
397 }
398
399 #[test]
400 fn diagnose_counts_connectivity() {
401 let report = Geodesic::default().diagnose(&Art::parse("========== ."));
402 assert_eq!(report.ink_cells, 11);
403 assert_eq!(report.connected_cells, 10); }
405
406 #[test]
410 fn directional_auto_accounts_for_cell_aspect() {
411 let tall = Art::parse("#####\n#####\n#####\n#####");
413 let r = Directional(Direction::Auto).rank(&tall);
414 assert!(
415 r.rank_at(0, 0).unwrap() < r.rank_at(0, 3).unwrap(),
416 "top first"
417 );
418 assert_eq!(
419 r.rank_at(0, 0),
420 r.rank_at(4, 0),
421 "same row reveals together"
422 );
423
424 let wide = Art::parse("##########\n##########");
426 let rw = Directional(Direction::Auto).rank(&wide);
427 assert!(
428 rw.rank_at(0, 0).unwrap() < rw.rank_at(9, 0).unwrap(),
429 "left first"
430 );
431 assert_eq!(
432 rw.rank_at(0, 0),
433 rw.rank_at(0, 1),
434 "same column reveals together"
435 );
436 }
437}