paths.rs
7.0 kB · rust · 223 lines
1use super::pens::{self, Pen};2use std::collections::BTreeSet;34const STEPS: [(i64, i64); 4] = [(-1, 0), (0, 1), (1, 0), (0, -1)];56/// Returns the character's hand-penned strokes from the pen tables, or None outside the font.7pub fn penned(c: char) -> Option<Vec<Vec<(usize, usize)>>> {8 pens::all()9 .into_iter()10 .find(|&(key, _)| key == c)11 .map(|pen| parse(&pen))12}1314/// Returns the character's ordered strokes over its trimmed bitmap, or none for a character outside the font.15pub fn strokes(c: char) -> Vec<Vec<(usize, usize)>> {16 penned(c).unwrap_or_default()17}1819/// Flattens the character's strokes into one cell-by-cell drawing order.20pub fn path(c: char) -> Vec<(usize, usize)> {21 strokes(c).into_iter().flatten().collect()22}2324fn parse((_, strokes): &Pen) -> Vec<Vec<(usize, usize)>> {25 strokes26 .iter()27 .map(|stroke| stroke.split_whitespace().map(cell).collect())28 .collect()29}3031fn cell(token: &str) -> (usize, usize) {32 let digits: Vec<usize> = token33 .chars()34 .map(|d| d.to_digit(10).unwrap() as usize)35 .collect();36 (digits[0], digits[1])37}3839fn lit_of(rows: &[String]) -> BTreeSet<(usize, usize)> {40 let mut lit = BTreeSet::new();41 for (r, row) in rows.iter().enumerate() {42 for (c, ch) in row.chars().enumerate() {43 if ch == '1' {44 lit.insert((r, c));45 }46 }47 }48 lit49}5051fn step(cell: (usize, usize), (dr, dc): (i64, i64)) -> Option<(usize, usize)> {52 let (r, c) = (cell.0 as i64 + dr, cell.1 as i64 + dc);53 (r >= 0 && c >= 0).then_some((r as usize, c as usize))54}5556fn degree(cell: (usize, usize), left: &BTreeSet<(usize, usize)>) -> usize {57 STEPS58 .iter()59 .filter(|&&d| step(cell, d).is_some_and(|n| left.contains(&n)))60 .count()61}6263fn opening(left: &BTreeSet<(usize, usize)>) -> (usize, usize) {64 *left65 .iter()66 .min_by_key(|&&cell| (degree(cell, left) != 1, usize::MAX - cell.0, cell.1))67 .expect("opening is only asked of a non-empty set")68}6970/// Drafts a stroke order for a trimmed bitmap by walking its lit cells: start at a lowest-left free end, keep heading, lift when stuck.71pub fn draft(rows: &[String]) -> Vec<Vec<(usize, usize)>> {72 let mut left = lit_of(rows);73 let mut out = Vec::new();74 while !left.is_empty() {75 let start = opening(&left);76 left.remove(&start);77 let mut stroke = vec![start];78 let mut heading: Option<(i64, i64)> = None;79 loop {80 let cur = *stroke.last().unwrap();81 let ahead = heading82 .and_then(|d| step(cur, d))83 .filter(|n| left.contains(n));84 let next = ahead.or_else(|| {85 STEPS86 .iter()87 .find_map(|&d| step(cur, d).filter(|n| left.contains(n)))88 });89 let Some(next) = next else { break };90 heading = Some((next.0 as i64 - cur.0 as i64, next.1 as i64 - cur.1 as i64));91 left.remove(&next);92 stroke.push(next);93 }94 out.push(stroke);95 }96 out97}9899/// Returns the least strokes that can write a trimmed bitmap: the minimum cover of its lit cells by 4-adjacent paths, zero for a blank.100pub fn floor(rows: &[String]) -> usize {101 let cells: Vec<(usize, usize)> = lit_of(rows).into_iter().collect();102 let n = cells.len();103 if n == 0 {104 return 0;105 }106 let index = |cell: (usize, usize)| cells.binary_search(&cell).ok();107 let mut edges = Vec::new();108 for (i, &(r, c)) in cells.iter().enumerate() {109 if let Some(j) = index((r, c + 1)) {110 edges.push((i, j));111 }112 if let Some(j) = index((r + 1, c)) {113 edges.push((i, j));114 }115 }116 let mut deg = vec![0u8; n];117 let mut parent: Vec<usize> = (0..n).collect();118 let mut best = n - draft(rows).len();119 forest(&edges, 0, 0, &mut deg, &mut parent, &mut best);120 n - best121}122123fn root(parent: &[usize], mut v: usize) -> usize {124 while parent[v] != v {125 v = parent[v];126 }127 v128}129130fn forest(131 edges: &[(usize, usize)],132 at: usize,133 taken: usize,134 deg: &mut [u8],135 parent: &mut [usize],136 best: &mut usize,137) {138 *best = (*best).max(taken);139 if at == edges.len() || taken + edges.len() - at <= *best {140 return;141 }142 let (a, b) = edges[at];143 if deg[a] < 2 && deg[b] < 2 {144 let (ra, rb) = (root(parent, a), root(parent, b));145 if ra != rb {146 deg[a] += 1;147 deg[b] += 1;148 parent[ra] = rb;149 forest(edges, at + 1, taken + 1, deg, parent, best);150 parent[ra] = ra;151 deg[a] -= 1;152 deg[b] -= 1;153 }154 }155 forest(edges, at + 1, taken, deg, parent, best);156}157158#[cfg(test)]159mod tests {160 use super::*;161 use crate::{glyph, supported, trim};162163 fn permutes(c: char, walk: &[(usize, usize)]) {164 let rows = trim(&glyph(c).unwrap().rows);165 let seen: BTreeSet<(usize, usize)> = walk.iter().copied().collect();166 assert_eq!(seen.len(), walk.len(), "{c} repeats a cell");167 assert_eq!(seen, lit_of(&rows), "{c} misses or invents a cell");168 }169170 #[test]171 fn every_path_is_a_permutation_of_the_glyph() {172 for c in supported() {173 permutes(c, &path(c));174 }175 }176177 #[test]178 fn every_draft_is_a_permutation_of_the_glyph() {179 for c in supported() {180 let rows = trim(&glyph(c).unwrap().rows);181 let walk: Vec<(usize, usize)> = draft(&rows).into_iter().flatten().collect();182 permutes(c, &walk);183 }184 }185186 #[test]187 fn the_pens_cover_the_font_once_in_order() {188 let keys: Vec<char> = pens::all().into_iter().map(|(c, _)| c).collect();189 assert_eq!(keys, supported(), "the pen tables drifted from the font");190 let distinct: BTreeSet<char> = keys.iter().copied().collect();191 assert_eq!(distinct.len(), keys.len(), "a glyph is penned twice");192 assert_eq!(strokes('M')[0][0], (4, 0));193 assert_eq!(strokes('D').len(), 4);194 assert_eq!(strokes('X').len(), 3);195 assert_eq!(strokes('8').len(), 1);196 }197198 #[test]199 fn no_pen_writes_below_its_floor() {200 for c in supported() {201 let rows = trim(&glyph(c).unwrap().rows);202 let least = floor(&rows);203 assert!(strokes(c).len() >= least, "{c} is penned under its floor");204 }205 let least = |c: char| floor(&trim(&glyph(c).unwrap().rows));206 assert_eq!((least('X'), least('8')), (3, 1));207 assert_eq!((least('#'), least('x'), least('*')), (8, 7, 13));208 assert_eq!((least(' '), least('.'), least('O')), (0, 1, 1));209 }210211 #[test]212 fn strokes_are_four_adjacent_walks() {213 for c in supported() {214 for stroke in strokes(c) {215 for pair in stroke.windows(2) {216 let (a, b) = (pair[0], pair[1]);217 let gap = a.0.abs_diff(b.0) + a.1.abs_diff(b.1);218 assert_eq!(gap, 1, "{c} jumps inside a stroke");219 }220 }221 }222 }223}