#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Piece {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub alpha: f32,
}
const LINES: &str = "\
0101 0202 1010 2020 0101 0202 1010 2020 0101 0202 1010 2020 0110 0210 0120 0220 \
0011 0012 0021 0022 1100 1200 2100 2200 1001 1002 2001 2002 1110 1210 2110 1120 \
2120 2210 1220 2220 1011 1012 2011 1021 2021 2012 1022 2022 0111 0112 0211 0212 \
0121 0122 0221 0222 1101 1102 1201 1202 2101 2102 2201 2202 1111 1112 1211 1212 \
2111 1121 2121 2112 2211 1122 1221 2212 1222 2122 2221 2222 0101 0202 1010 2020 \
0303 3030 0310 0130 0330 0013 0031 0033 1300 3100 3300 1003 3001 3003 1310 3130 \
3330 1013 3031 3033 0313 0131 0333 1303 3101 3303 1313 3131 3333 0110 0011 1001 \
1100 .... .... .... 0001 1000 0100 0010 0002 2000 0200 0020 0210 1020 0102 2010";
fn line_weights(ch: char) -> Option<[u8; 4]> {
if !('\u{2500}'..='\u{257F}').contains(&ch) {
return None;
}
let index = (ch as u32 - 0x2500) as usize;
let entry = LINES.split(' ').nth(index)?;
if entry.starts_with('.') {
return None;
}
let b = entry.as_bytes();
Some([b[0] - b'0', b[1] - b'0', b[2] - b'0', b[3] - b'0'])
}
pub fn is_drawn(ch: char) -> bool {
matches!(ch as u32, 0x2580..=0x259F) || line_weights(ch).is_some()
}
pub fn pieces(ch: char, x: f32, y: f32, w: f32, h: f32, light: f32) -> Option<Vec<Piece>> {
if let Some(weights) = line_weights(ch) {
return Some(lines(weights, x, y, w, h, light));
}
blocks(ch, x, y, w, h)
}
fn lines(weights: [u8; 4], x: f32, y: f32, w: f32, h: f32, light: f32) -> Vec<Piece> {
let mut out = Vec::new();
let cx = (x + w / 2.0).round();
let cy = (y + h / 2.0).round();
let (x0, x1, y0, y1) = (x.round(), (x + w).round(), y.round(), (y + h).round());
let mut rect = |ax: f32, ay: f32, bx: f32, by: f32| {
let (l, r) = (ax.min(bx), ax.max(bx));
let (t, b) = (ay.min(by), ay.max(by));
out.push(Piece {
x: l,
y: t,
w: r - l,
h: b - t,
alpha: 1.0,
});
};
let [up, right, down, left] = weights;
let thickness = |wt: u8| if wt == 2 { light * 2.0 } else { light };
for (dir, wt) in [(0, up), (1, right), (2, down), (3, left)] {
if wt == 0 {
continue;
}
let t = thickness(wt);
let offsets: &[f32] = if wt == 3 { &[-1.5, 0.5] } else { &[-0.5] };
let reach = if wt == 3 { 1.5 * light } else { t / 2.0 };
for off in offsets {
let o = (off * t).round();
match dir {
0 => rect(cx + o, y0, cx + o + t, cy + reach),
1 => rect(cx - reach, cy + o, x1, cy + o + t),
2 => rect(cx + o, cy - reach, cx + o + t, y1),
_ => rect(x0, cy + o, cx + reach, cy + o + t),
}
}
}
out
}
fn blocks(ch: char, x: f32, y: f32, w: f32, h: f32) -> Option<Vec<Piece>> {
let frac = |l: f32, t: f32, fw: f32, fh: f32, alpha: f32| {
let (ax, ay) = ((x + l * w).round(), (y + t * h).round());
let (bx, by) = ((x + (l + fw) * w).round(), (y + (t + fh) * h).round());
Piece {
x: ax,
y: ay,
w: bx - ax,
h: by - ay,
alpha,
}
};
let code = ch as u32;
let quads = |ul: bool, ur: bool, ll: bool, lr: bool| {
let mut v = Vec::new();
for (on, l, t) in [
(ul, 0.0, 0.0),
(ur, 0.5, 0.0),
(ll, 0.0, 0.5),
(lr, 0.5, 0.5),
] {
if on {
v.push(frac(l, t, 0.5, 0.5, 1.0));
}
}
v
};
Some(match code {
0x2580 => vec![frac(0.0, 0.0, 1.0, 0.5, 1.0)],
0x2581..=0x2588 => {
let n = (code - 0x2580) as f32 / 8.0;
vec![frac(0.0, 1.0 - n, 1.0, n, 1.0)]
}
0x2589..=0x258F => {
let n = (0x2590 - code) as f32 / 8.0;
vec![frac(0.0, 0.0, n, 1.0, 1.0)]
}
0x2590 => vec![frac(0.5, 0.0, 0.5, 1.0, 1.0)],
0x2591 => vec![frac(0.0, 0.0, 1.0, 1.0, 0.25)],
0x2592 => vec![frac(0.0, 0.0, 1.0, 1.0, 0.5)],
0x2593 => vec![frac(0.0, 0.0, 1.0, 1.0, 0.75)],
0x2594 => vec![frac(0.0, 0.0, 1.0, 0.125, 1.0)],
0x2595 => vec![frac(0.875, 0.0, 0.125, 1.0, 1.0)],
0x2596 => quads(false, false, true, false),
0x2597 => quads(false, false, false, true),
0x2598 => quads(true, false, false, false),
0x2599 => quads(true, false, true, true),
0x259A => quads(true, false, false, true),
0x259B => quads(true, true, true, false),
0x259C => quads(true, true, false, true),
0x259D => quads(false, true, false, false),
0x259E => quads(false, true, true, false),
0x259F => quads(false, true, true, true),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_covers_the_whole_box_drawing_block() {
assert_eq!(LINES.split(' ').count(), 0x80);
assert_eq!(line_weights('─'), Some([0, 1, 0, 1]));
assert_eq!(line_weights('┼'), Some([1, 1, 1, 1]));
assert_eq!(line_weights('╋'), Some([2, 2, 2, 2]));
assert_eq!(line_weights('╔'), Some([0, 3, 3, 0]));
assert_eq!(line_weights('╬'), Some([3, 3, 3, 3]));
assert_eq!(line_weights('╭'), Some([0, 1, 1, 0]));
assert_eq!(line_weights('╿'), Some([2, 0, 1, 0]));
assert_eq!(line_weights('╱'), None);
assert!(!is_drawn('╱'));
assert!(!is_drawn('a'));
assert!(is_drawn('█'));
}
#[test]
fn horizontal_lines_span_the_full_cell_width() {
let p = pieces('─', 10.0, 20.0, 8.0, 16.0, 1.0).unwrap();
let left = p.iter().map(|r| r.x).fold(f32::MAX, f32::min);
let right = p.iter().map(|r| r.x + r.w).fold(f32::MIN, f32::max);
assert_eq!((left, right), (10.0, 18.0));
assert!(p.iter().all(|r| r.h == 1.0));
}
#[test]
fn vertical_lines_span_the_full_cell_height() {
let p = pieces('│', 0.0, 16.0, 8.0, 16.0, 1.0).unwrap();
let top = p.iter().map(|r| r.y).fold(f32::MAX, f32::min);
let bottom = p.iter().map(|r| r.y + r.h).fold(f32::MIN, f32::max);
assert_eq!((top, bottom), (16.0, 32.0));
}
#[test]
fn double_lines_have_two_strokes() {
assert_eq!(pieces('═', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap().len(), 4);
}
#[test]
fn blocks_and_shades() {
let full = pieces('█', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap();
assert_eq!(
full,
vec![Piece {
x: 0.0,
y: 0.0,
w: 8.0,
h: 16.0,
alpha: 1.0
}]
);
let lower = pieces('▄', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap();
assert_eq!((lower[0].y, lower[0].h), (8.0, 8.0));
let left = pieces('▌', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap();
assert_eq!(left[0].w, 4.0);
assert_eq!(pieces('▒', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap()[0].alpha, 0.5);
assert_eq!(pieces('▚', 0.0, 0.0, 8.0, 16.0, 1.0).unwrap().len(), 2);
}
}