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)]
144pub struct Geodesic {
145 pub start: StartHint,
147 pub bridge: u16,
152}
153
154impl Default for Geodesic {
155 fn default() -> Self {
158 Geodesic {
159 start: StartHint::default(),
160 bridge: 1,
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
167pub enum StartHint {
168 #[default]
170 TopLeft,
171 Bottom,
173 Topological,
175}
176
177#[derive(Clone, Copy, Debug)]
182pub struct GeodesicReport {
183 pub ink_cells: usize,
184 pub connected_cells: usize,
186 pub spine_length: u32,
188}
189
190impl Geodesic {
191 pub fn diagnose(&self, art: &Art) -> GeodesicReport {
193 let mask = ink_mask(art);
194 match Spine::solve(&mask, art.width(), art.height(), self.start, self.bridge) {
195 Some(spine) => GeodesicReport {
196 ink_cells: art.ink_count(),
197 connected_cells: spine.dist.iter().filter(|d| d.is_some()).count(),
198 spine_length: spine.diameter,
199 },
200 None => GeodesicReport {
201 ink_cells: 0,
202 connected_cells: 0,
203 spine_length: 0,
204 },
205 }
206 }
207}
208
209impl Ordering for Geodesic {
210 fn rank(&self, art: &Art) -> RankMap {
211 let (w, h) = (art.width(), art.height());
212 let mut map = RankMap::new(w, h);
213 if art.ink_count() == 0 {
214 return map;
215 }
216
217 let skel = skeletonize(art);
220 let value = skeleton_values(&skel, w, h, self.start, self.bridge);
221
222 let mut val = value.clone();
228 let mut depth = vec![0u32; val.len()];
229 let mut queue: VecDeque<usize> = (0..val.len()).filter(|&i| !val[i].is_nan()).collect();
230 while let Some(cur) = queue.pop_front() {
231 for ni in neighbours(cur, w, h) {
232 if val[ni].is_nan() {
233 val[ni] = val[cur];
234 depth[ni] = depth[cur] + 1;
235 queue.push_back(ni);
236 }
237 }
238 }
239
240 let mut order: Vec<(u16, u16, f32, u32)> = art
244 .ink_cells()
245 .map(|c| {
246 let i = art.index(c.x, c.y);
247 (c.x, c.y, val[i], depth[i])
248 })
249 .collect();
250 order.sort_by(|a, b| a.2.total_cmp(&b.2).then(a.3.cmp(&b.3)));
251 let denom = order.len().saturating_sub(1).max(1) as f32;
252 for (i, &(x, y, _, _)) in order.iter().enumerate() {
253 map.set(x, y, i as f32 / denom);
254 }
255 map
256 }
257}
258
259const STRICT_CONNECTED_MIN: f32 = 0.6;
262
263struct Spine {
265 dist: Vec<Option<u32>>,
268 diameter: u32,
270}
271
272impl Spine {
273 fn solve(mask: &[bool], w: u16, h: u16, hint: StartHint, bridge: u16) -> Option<Self> {
274 let count = mask.iter().filter(|&&m| m).count();
279 let strict_seed = largest_component_seed(mask, w, h, 0)?;
280 let strict_size = bfs(mask, w, h, strict_seed, 0)
281 .0
282 .iter()
283 .filter(|d| d.is_some())
284 .count();
285 let bridge = if (strict_size as f32) < STRICT_CONNECTED_MIN * count.max(1) as f32 {
286 bridge
287 } else {
288 0
289 };
290
291 let seed = if bridge == 0 {
293 strict_seed
294 } else {
295 largest_component_seed(mask, w, h, bridge)?
296 };
297 let (_, far_a) = bfs(mask, w, h, seed, bridge);
298 let (dist_a, far_b) = bfs(mask, w, h, far_a, bridge);
299 let (dist_b, _) = bfs(mask, w, h, far_b, bridge);
300
301 let coord = |i: usize| ((i % w as usize) as u16, (i / w as usize) as u16);
303 let (ax, ay) = coord(far_a);
304 let (bx, by) = coord(far_b);
305 let start_is_a = match hint {
306 StartHint::Topological => true,
307 StartHint::TopLeft => (ay, ax) <= (by, bx),
308 StartHint::Bottom => ay >= by,
309 };
310
311 let dist = if start_is_a { dist_a } else { dist_b };
312 let diameter = dist.iter().flatten().copied().max().unwrap_or(0);
313 Some(Spine { dist, diameter })
314 }
315}
316
317fn neighbours(index: usize, w: u16, h: u16) -> impl Iterator<Item = usize> {
323 let (w, h) = (w as i32, h as i32);
324 let (cx, cy) = ((index as i32 % w), (index as i32 / w));
325 (-1..=1)
326 .flat_map(move |dy| (-1..=1).map(move |dx| (dx, dy)))
327 .filter_map(move |(dx, dy)| {
328 if dx == 0 && dy == 0 {
329 return None;
330 }
331 let (nx, ny) = (cx + dx, cy + dy);
332 (nx >= 0 && ny >= 0 && nx < w && ny < h).then_some((ny * w + nx) as usize)
333 })
334}
335
336fn ink_mask(art: &Art) -> Vec<bool> {
338 let (w, h) = (art.width() as usize, art.height() as usize);
339 (0..w * h)
340 .map(|i| art.is_ink((i % w) as u16, (i / w) as u16))
341 .collect()
342}
343
344fn largest_component_seed(mask: &[bool], w: u16, h: u16, bridge: u16) -> Option<usize> {
347 let mut visited = vec![false; mask.len()];
348 let mut queue = VecDeque::new();
349 let mut best: Option<(usize, usize)> = None; for seed in 0..mask.len() {
352 if !mask[seed] || visited[seed] {
353 continue;
354 }
355 let mut size = 0usize;
356 visited[seed] = true;
357 queue.push_back(seed);
358 while let Some(cur) = queue.pop_front() {
359 size += 1;
360 for ni in bridged_neighbours(mask, w, h, cur, bridge) {
361 if !visited[ni] {
362 visited[ni] = true;
363 queue.push_back(ni);
364 }
365 }
366 }
367 if best.map_or(true, |(best_size, _)| size > best_size) {
368 best = Some((size, seed));
369 }
370 }
371 best.map(|(_, seed)| seed)
372}
373
374fn bfs(mask: &[bool], w: u16, h: u16, source: usize, bridge: u16) -> (Vec<Option<u32>>, usize) {
378 let mut dist = vec![None; mask.len()];
379 let mut queue = VecDeque::new();
380
381 dist[source] = Some(0);
382 queue.push_back(source);
383 let (mut farthest, mut far_d) = (source, 0u32);
384
385 while let Some(cur) = queue.pop_front() {
386 let d = dist[cur].unwrap();
387 if d > far_d {
388 far_d = d;
389 farthest = cur;
390 }
391 for ni in bridged_neighbours(mask, w, h, cur, bridge) {
392 if dist[ni].is_none() {
393 dist[ni] = Some(d + 1);
394 queue.push_back(ni);
395 }
396 }
397 }
398 (dist, farthest)
399}
400
401fn bridged_neighbours(mask: &[bool], w: u16, h: u16, index: usize, bridge: u16) -> Vec<usize> {
404 let (wi, hi) = (w as i32, h as i32);
405 let r = bridge as i32 + 1;
406 let (cx, cy) = (index as i32 % wi, index as i32 / wi);
407 let mut out = Vec::new();
408 for dy in -r..=r {
409 for dx in -r..=r {
410 if dx == 0 && dy == 0 {
411 continue;
412 }
413 let (nx, ny) = (cx + dx, cy + dy);
414 if nx >= 0 && ny >= 0 && nx < wi && ny < hi {
415 let ni = (ny * wi + nx) as usize;
416 if mask[ni] {
417 out.push(ni);
418 }
419 }
420 }
421 }
422 out
423}
424
425fn skeletonize(art: &Art) -> Vec<bool> {
429 let (w, h) = (art.width() as i32, art.height() as i32);
430 let idx = |x: i32, y: i32| (y * w + x) as usize;
431 let mut g = ink_mask(art);
432 let val = |g: &[bool], x: i32, y: i32| -> u8 {
433 (x >= 0 && y >= 0 && x < w && y < h && g[idx(x, y)]) as u8
434 };
435 loop {
436 let mut removed = false;
437 for step in 0..2 {
438 let mut marks = Vec::new();
439 for y in 0..h {
440 for x in 0..w {
441 if !g[idx(x, y)] {
442 continue;
443 }
444 let p = [
446 val(&g, x, y - 1),
447 val(&g, x + 1, y - 1),
448 val(&g, x + 1, y),
449 val(&g, x + 1, y + 1),
450 val(&g, x, y + 1),
451 val(&g, x - 1, y + 1),
452 val(&g, x - 1, y),
453 val(&g, x - 1, y - 1),
454 ];
455 let b: u8 = p.iter().sum();
456 if !(2..=6).contains(&b) {
457 continue;
458 }
459 let a = (0..8).filter(|&i| p[i] == 0 && p[(i + 1) % 8] == 1).count();
460 if a != 1 {
461 continue;
462 }
463 let (c1, c2) = if step == 0 {
464 (p[0] * p[2] * p[4], p[2] * p[4] * p[6])
465 } else {
466 (p[0] * p[2] * p[6], p[0] * p[4] * p[6])
467 };
468 if c1 == 0 && c2 == 0 {
469 marks.push(idx(x, y));
470 }
471 }
472 }
473 if !marks.is_empty() {
474 removed = true;
475 for i in marks {
476 g[i] = false;
477 }
478 }
479 }
480 if !removed {
481 break;
482 }
483 }
484 g
485}
486
487fn skeleton_values(skel: &[bool], w: u16, h: u16, hint: StartHint, bridge: u16) -> Vec<f32> {
492 let mut value = vec![f32::NAN; skel.len()];
493 let count = skel.iter().filter(|&&m| m).count();
494 if count == 0 {
495 return value;
496 }
497
498 let bridge = match largest_component_seed(skel, w, h, 0) {
501 Some(seed)
502 if bfs(skel, w, h, seed, 0)
503 .0
504 .iter()
505 .filter(|d| d.is_some())
506 .count() as f32
507 >= STRICT_CONNECTED_MIN * count as f32 =>
508 {
509 0
510 }
511 _ => bridge,
512 };
513
514 let mut comp_id = vec![usize::MAX; skel.len()];
516 let mut comps: Vec<Vec<usize>> = Vec::new();
517 for i in 0..skel.len() {
518 if !skel[i] || comp_id[i] != usize::MAX {
519 continue;
520 }
521 let id = comps.len();
522 let mut cells = Vec::new();
523 let mut queue = VecDeque::new();
524 comp_id[i] = id;
525 queue.push_back(i);
526 while let Some(cur) = queue.pop_front() {
527 cells.push(cur);
528 for ni in bridged_neighbours(skel, w, h, cur, bridge) {
529 if comp_id[ni] == usize::MAX {
530 comp_id[ni] = id;
531 queue.push_back(ni);
532 }
533 }
534 }
535 comps.push(cells);
536 }
537
538 let horizontal = w as u32 > 2 * h as u32;
539 let axis = |i: usize| -> u16 {
540 if horizontal {
541 (i % w as usize) as u16
542 } else {
543 (i / w as usize) as u16
544 }
545 };
546 let coord = |i: usize| ((i % w as usize) as u16, (i / w as usize) as u16);
547
548 let mut pieces: Vec<(u16, Vec<(usize, f32)>)> = comps
550 .iter()
551 .map(|comp| {
552 let (_, far_a) = bfs(skel, w, h, comp[0], bridge);
553 let (dist_a, far_b) = bfs(skel, w, h, far_a, bridge);
554 let (dist_b, _) = bfs(skel, w, h, far_b, bridge);
555 let (ax, ay) = coord(far_a);
556 let (bx, by) = coord(far_b);
557 let start_is_a = match hint {
558 StartHint::Topological => true,
559 StartHint::TopLeft => (ay, ax) <= (by, bx),
560 StartHint::Bottom => ay >= by,
561 };
562 let dist = if start_is_a { dist_a } else { dist_b };
563 let diameter = dist.iter().flatten().copied().max().unwrap_or(0).max(1) as f32;
564 let lead = comp.iter().map(|&c| axis(c)).min().unwrap_or(0);
565 let within = comp
566 .iter()
567 .map(|&c| (c, dist[c].map_or(0.0, |d| d as f32 / diameter)))
568 .collect();
569 (lead, within)
570 })
571 .collect();
572
573 pieces.sort_by_key(|(lead, _)| *lead);
574 for (piece, (_, within)) in pieces.iter().enumerate() {
575 for &(cell, w) in within {
576 value[cell] = piece as f32 + w;
577 }
578 }
579 value
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
589 fn straight_line_reveals_along_itself() {
590 let art = Art::parse("=========");
591 let ranks = Geodesic::default().rank(&art);
592 let row: Vec<f32> = (0..art.width())
593 .map(|x| ranks.rank_at(x, 0).unwrap())
594 .collect();
595 let increasing = row.windows(2).all(|w| w[0] <= w[1]);
596 let decreasing = row.windows(2).all(|w| w[0] >= w[1]);
597 assert!(
598 increasing || decreasing,
599 "spine reveal was not monotone: {row:?}"
600 );
601 assert!((row.iter().cloned().fold(0.0_f32, f32::max) - 1.0).abs() < 1e-6);
602 }
603
604 #[test]
606 fn spine_traces_largest_component() {
607 let art = Art::parse(".\n\n ========");
608 let report = Geodesic::default().diagnose(&art);
609 assert_eq!(report.ink_cells, 9);
610 assert_eq!(report.connected_cells, 8); }
612
613 #[test]
616 fn islands_inherit_nearest_spine_rank() {
617 let art = Art::parse(". ====== .");
618 let ranks = Geodesic::default().rank(&art);
619 let left = ranks.rank_at(0, 0).unwrap();
620 let right = ranks.rank_at(11, 0).unwrap();
621 assert!(left < right, "left {left} should precede right {right}");
622 assert!(left < 0.25 && right > 0.75, "left={left} right={right}");
623 }
624
625 #[test]
626 fn diagnose_counts_connectivity() {
627 let report = Geodesic::default().diagnose(&Art::parse("========== ."));
628 assert_eq!(report.ink_cells, 11);
629 assert_eq!(report.connected_cells, 10); }
631
632 #[test]
635 fn bridges_small_gaps_when_fragmented() {
636 let art = Art::parse("== ==");
637 let strict = Geodesic {
638 start: StartHint::TopLeft,
639 bridge: 0,
640 };
641 assert_eq!(strict.diagnose(&art).connected_cells, 2);
642 assert_eq!(Geodesic::default().diagnose(&art).connected_cells, 4);
643 }
644
645 #[test]
648 fn connected_art_is_not_bridged() {
649 let art = Art::parse("####\n #\n####\n#\n####");
652 let report = Geodesic::default().diagnose(&art);
653 assert_eq!(report.connected_cells, report.ink_cells);
654 assert!(
655 report.spine_length >= 9,
656 "spine was {}",
657 report.spine_length
658 );
659 }
660
661 #[test]
664 fn solid_block_reveals_across_the_whole_bar() {
665 let art = Art::parse(&"########\n".repeat(8));
666 let r = Geodesic::default().rank(&art);
667 let ranks: Vec<f32> = (0..8)
668 .flat_map(|y| (0..8u16).map(move |x| (x, y)))
669 .map(|(x, y)| r.rank_at(x, y).unwrap())
670 .collect();
671 let lo = ranks.iter().cloned().fold(f32::MAX, f32::min);
672 let hi = ranks.iter().cloned().fold(f32::MIN, f32::max);
673 assert!(
674 lo < 0.02 && hi > 0.98,
675 "block did not use the whole bar: {lo}..{hi}"
676 );
677 }
678
679 #[test]
682 fn separate_pieces_reveal_in_reading_order() {
683 let art = Art::parse("## ##\n## ##\n## ##");
684 let r = Geodesic::default().rank(&art);
685 let left = r.rank_at(0, 1).unwrap();
686 let right = r.rank_at(11, 1).unwrap();
687 assert!(
688 left < right,
689 "left piece {left} should precede right {right}"
690 );
691 assert!(
692 left < 0.5 && right > 0.5,
693 "pieces out of order: {left} {right}"
694 );
695 }
696
697 #[test]
700 fn thin_line_stays_a_trace() {
701 let art = Art::parse("==============");
702 let r = Geodesic::default().rank(&art);
703 let row: Vec<f32> = (0..art.width()).map(|x| r.rank_at(x, 0).unwrap()).collect();
704 let lo = row.iter().cloned().fold(f32::MAX, f32::min);
705 let hi = row.iter().cloned().fold(f32::MIN, f32::max);
706 assert!(
707 lo < 0.01 && hi > 0.99,
708 "line did not trace end to end: {row:?}"
709 );
710 }
711
712 #[test]
716 fn directional_auto_accounts_for_cell_aspect() {
717 let tall = Art::parse("#####\n#####\n#####\n#####");
719 let r = Directional(Direction::Auto).rank(&tall);
720 assert!(
721 r.rank_at(0, 0).unwrap() < r.rank_at(0, 3).unwrap(),
722 "top first"
723 );
724 assert_eq!(
725 r.rank_at(0, 0),
726 r.rank_at(4, 0),
727 "same row reveals together"
728 );
729
730 let wide = Art::parse("##########\n##########");
732 let rw = Directional(Direction::Auto).rank(&wide);
733 assert!(
734 rw.rank_at(0, 0).unwrap() < rw.rank_at(9, 0).unwrap(),
735 "left first"
736 );
737 assert_eq!(
738 rw.rank_at(0, 0),
739 rw.rank_at(0, 1),
740 "same column reveals together"
741 );
742 }
743}