use crate::cli::Args;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Charset {
HalfBlock,
HalfBlockDual,
Ascii,
Braille,
Quadrant,
Shade,
Points,
Sculpted,
CustomAscii(Vec<char>),
}
impl Charset {
pub fn from_args(args: &Args) -> Self {
if args.sculpted {
Charset::Sculpted
} else if args.quadrant {
Charset::Quadrant
} else if args.shade {
Charset::Shade
} else if args.points {
Charset::Points
} else if args.braille {
Charset::Braille
} else if args.half_block_dual {
Charset::HalfBlockDual
} else if let Some(ref custom_chars) = args.ascii_chars {
Charset::from_custom_string(custom_chars)
} else if args.ascii {
Charset::Ascii
} else {
Charset::HalfBlockDual
}
}
pub fn from_custom_string(chars: &str) -> Self {
let mut unique_chars: Vec<char> = chars.chars().collect();
unique_chars.dedup();
unique_chars.sort_by(|a, b| {
estimate_char_density(*a)
.partial_cmp(&estimate_char_density(*b))
.unwrap_or(std::cmp::Ordering::Equal)
});
Charset::CustomAscii(unique_chars)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GlyphSelection {
Brightness,
Shape,
Hybrid,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphConfig {
pub selection: Option<GlyphSelection>,
pub edge_threshold: f32,
}
impl Default for GlyphConfig {
fn default() -> Self {
Self {
selection: None,
edge_threshold: crate::config_defaults::glyph_consts::DEFAULT_GLYPH_EDGE_THRESHOLD,
}
}
}
pub fn sobel_edge_glyph(n: &[f32; 9], threshold: f32) -> Option<char> {
let gx = ((n[2] + 2.0 * n[5] + n[8]) - (n[0] + 2.0 * n[3] + n[6])) / 4.0;
let gy = ((n[6] + 2.0 * n[7] + n[8]) - (n[0] + 2.0 * n[1] + n[2])) / 4.0;
let mag = (gx * gx + gy * gy).sqrt();
if mag <= threshold {
return None;
}
let mut a = gx.atan2(gy).to_degrees();
if a < 0.0 {
a += 180.0;
}
let glyph = if !(22.5..157.5).contains(&a) {
'-'
} else if a < 67.5 {
'/'
} else if a < 112.5 {
'|'
} else {
'\\'
};
Some(glyph)
}
pub const ALL_CHARSETS: [Charset; 7] = [
Charset::HalfBlock,
Charset::HalfBlockDual,
Charset::Ascii,
Charset::Braille,
Charset::Quadrant,
Charset::Shade,
Charset::Points,
];
pub const NUM_CHARSETS: usize = ALL_CHARSETS.len();
const HALF_BLOCK_CHARS: [char; 9] = [
' ', '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
'\u{2588}',
];
const ASCII_CHARS: [char; 10] = [' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'];
const ASCII_TOP_CHARS: [char; 10] = [' ', ',', '.', ':', '-', '=', '+', '*', '#', '^'];
const ASCII_BOTTOM_CHARS: [char; 10] = [' ', '\'', '.', ':', '-', '=', '+', '*', '#', 'v'];
const BRAILLE_DOT_MASKS: [u8; 5] = [
0x00, 0x01, 0x03, 0x07, 0x07, ];
const SHADE_CHARS: [char; 5] = [' ', '░', '▒', '▓', '█'];
const POINT_CHAR: char = '▪';
#[inline]
fn estimate_char_density(c: char) -> f32 {
match c {
' ' => 0.0,
'\t' | '\n' => 0.0,
'.' | ',' | '\'' | '`' | '´' | '°' | '·' | '˙' => 0.1,
':' | ';' | '!' | '|' | 'i' | 'l' | 'I' | '1' => 0.2,
'/' | '\\' | '(' | ')' | '[' | ']' | '{' | '}' => 0.25,
'-' | '_' | '~' | '^' | '"' | 'r' | 'c' | 'v' | 'f' | 'j' | 't' => 0.3,
'<' | '>' | 'L' | 'T' | 'Y' | 'J' | '7' => 0.35,
'=' | '+' | '?' | 's' | 'z' | 'x' | 'k' | 'n' | 'u' | 'y' => 0.4,
'a' | 'e' | 'o' | 'h' | 'p' | 'q' | 'b' | 'd' | 'g' => 0.45,
'F' | 'P' | 'E' | 'Z' | '2' | '3' | '5' => 0.5,
'*' | '×' | 'w' | 'm' | 'V' | 'X' | 'K' => 0.55,
'A' | 'S' | 'C' | 'U' | 'R' | 'G' | '4' | '6' | '9' => 0.6,
'#' | 'H' | 'N' | 'D' | 'B' | 'O' | 'Q' | '8' | '0' => 0.7,
'W' | 'M' => 0.75,
'@' | '%' | '&' | '$' => 0.8,
'▓' | '▒' => 0.9,
'■' | '▪' | '▬' => 0.95,
'█' => 1.0, '\u{2587}' => 0.875,
'\u{2586}' => 0.75,
'\u{2585}' => 0.625,
'\u{2584}' => 0.5,
'\u{2583}' => 0.375,
'\u{2582}' => 0.25,
'\u{2581}' => 0.125,
_ => {
if c.is_ascii_uppercase() {
0.55
} else if c.is_ascii_lowercase() {
0.4
} else if c.is_ascii_digit() {
0.5
} else if c.is_ascii_punctuation() {
0.3
} else {
0.5
}
}
}
}
pub fn map_braille_subpixel(top: f32, bottom: f32, threshold: f32) -> char {
let top = top.clamp(0.0, 1.0);
let bottom = bottom.clamp(0.0, 1.0);
let top_level = if top < threshold {
0
} else {
((top - threshold) / (1.0 - threshold) * 4.0).ceil() as usize
}
.min(4);
let bottom_level = if bottom < threshold {
0
} else {
((bottom - threshold) / (1.0 - threshold) * 4.0).ceil() as usize
}
.min(4);
let left_mask = BRAILLE_DOT_MASKS[top_level];
let right_mask = BRAILLE_DOT_MASKS[bottom_level] << 3;
let combined_mask = left_mask | right_mask;
char::from_u32(0x2800 + combined_mask as u32).unwrap_or(' ')
}
pub fn map_shade(brightness: f32) -> char {
let b = brightness.clamp(0.0, 1.0);
let index = (b * (SHADE_CHARS.len() - 1) as f32).round() as usize;
SHADE_CHARS[index]
}
pub fn map_point(brightness: f32, threshold: f32) -> char {
if brightness > threshold {
POINT_CHAR
} else {
' '
}
}
pub fn map_quadrant(
top_left: f32,
top_right: f32,
bottom_left: f32,
bottom_right: f32,
threshold: f32,
) -> char {
let threshold = threshold.clamp(0.0, 1.0);
let tl = top_left > threshold;
let tr = top_right > threshold;
let bl = bottom_left > threshold;
let br = bottom_right > threshold;
let index = (tl as u32) | ((tr as u32) << 1) | ((bl as u32) << 2) | ((br as u32) << 3);
match index {
0x0 => ' ', 0x1 => '\u{2598}', 0x2 => '\u{259D}', 0x3 => '\u{2580}', 0x4 => '\u{2596}', 0x5 => '\u{258C}', 0x6 => '\u{259E}', 0x7 => '\u{259B}', 0x8 => '\u{2597}', 0x9 => '\u{259A}', 0xA => '\u{2590}', 0xB => '\u{259C}', 0xC => '\u{2584}', 0xD => '\u{2599}', 0xE => '\u{259F}', 0xF => '\u{2588}', _ => ' ',
}
}
pub fn map_brightness(top: f32, bottom: Option<f32>, charset: Charset) -> char {
match charset {
Charset::HalfBlock | Charset::HalfBlockDual => {
let brightness = top.clamp(0.0, 1.0);
let index = (brightness * (HALF_BLOCK_CHARS.len() - 1) as f32).round() as usize;
HALF_BLOCK_CHARS[index]
}
Charset::Ascii => {
let brightness = top.clamp(0.0, 1.0);
let index = (brightness * (ASCII_CHARS.len() - 1) as f32).round() as usize;
ASCII_CHARS[index]
}
Charset::Braille => {
use crate::config_defaults::threshold;
if let Some(bottom_val) = bottom {
map_braille_subpixel(top, bottom_val, threshold::DEFAULT_BRAILLE_THRESHOLD)
} else {
let brightness = top.clamp(0.0, 1.0);
let index = (brightness * 15.0).round() as usize;
char::from_u32(0x2800 + index as u32).unwrap_or(' ')
}
}
Charset::Quadrant => {
let brightness = top.clamp(0.0, 1.0);
if brightness < 0.25 {
' '
} else if brightness < 0.5 {
'\u{1FB00}' } else if brightness < 0.75 {
'\u{1FB02}' } else {
'\u{1FB0E}' }
}
Charset::Shade => {
let avg = if let Some(bottom_val) = bottom {
(top + bottom_val) / 2.0
} else {
top
};
map_shade(avg)
}
Charset::Points => {
use crate::config_defaults::threshold;
let avg = if let Some(bottom_val) = bottom {
(top + bottom_val) / 2.0
} else {
top
};
map_point(avg, threshold::DEFAULT_POINT_THRESHOLD) }
Charset::Sculpted => {
let brightness = top.clamp(0.0, 1.0);
let index = (brightness * (HALF_BLOCK_CHARS.len() - 1) as f32).round() as usize;
HALF_BLOCK_CHARS[index]
}
Charset::CustomAscii(ref chars) => {
if chars.is_empty() {
return ' ';
}
let brightness = top.clamp(0.0, 1.0);
let index = (brightness * (chars.len() - 1) as f32).round() as usize;
chars[index.min(chars.len() - 1)]
}
}
}
pub fn map_half_block_dual(top: f32, bottom: f32, threshold: f32) -> char {
let top_above = top > threshold;
let bottom_above = bottom > threshold;
match (top_above, bottom_above) {
(true, true) => '▀', (true, false) => '▀', (false, true) => '▄', (false, false) => ' ',
}
}
pub fn map_vertical_block(top: f32, bottom: f32) -> char {
use crate::config_defaults::threshold;
let top_above = top > threshold::DEFAULT_VERTICAL_BLOCK_THRESHOLD;
let bottom_above = bottom > threshold::DEFAULT_VERTICAL_BLOCK_THRESHOLD;
match (top_above, bottom_above) {
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
(false, false) => ' ',
}
}
pub fn map_ascii_directional(brightness: f32, is_top: bool) -> char {
let brightness = brightness.clamp(0.0, 1.0);
let chars = if is_top {
ASCII_TOP_CHARS
} else {
ASCII_BOTTOM_CHARS
};
let index = (brightness * (chars.len() - 1) as f32).round() as usize;
chars[index]
}
struct ShapeEntry {
ch: char,
vector: [f32; 6],
}
const SHAPE_TABLE: [ShapeEntry; 55] = [
ShapeEntry {
ch: ' ',
vector: [0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
},
ShapeEntry {
ch: '.',
vector: [0.00, 0.00, 0.00, 0.00, 0.05, 0.05],
},
ShapeEntry {
ch: ',',
vector: [0.00, 0.00, 0.00, 0.00, 0.02, 0.10],
},
ShapeEntry {
ch: '\'',
vector: [0.05, 0.08, 0.00, 0.00, 0.00, 0.00],
},
ShapeEntry {
ch: '`',
vector: [0.10, 0.03, 0.00, 0.00, 0.00, 0.00],
},
ShapeEntry {
ch: '"',
vector: [0.10, 0.10, 0.00, 0.00, 0.00, 0.00],
},
ShapeEntry {
ch: ':',
vector: [0.00, 0.00, 0.08, 0.08, 0.08, 0.08],
},
ShapeEntry {
ch: ';',
vector: [0.00, 0.00, 0.06, 0.06, 0.04, 0.10],
},
ShapeEntry {
ch: '-',
vector: [0.00, 0.00, 0.35, 0.35, 0.00, 0.00],
},
ShapeEntry {
ch: '_',
vector: [0.00, 0.00, 0.00, 0.00, 0.35, 0.35],
},
ShapeEntry {
ch: '~',
vector: [0.00, 0.00, 0.28, 0.28, 0.00, 0.00],
},
ShapeEntry {
ch: '=',
vector: [0.12, 0.12, 0.30, 0.30, 0.12, 0.12],
},
ShapeEntry {
ch: '|',
vector: [0.15, 0.15, 0.18, 0.18, 0.15, 0.15],
},
ShapeEntry {
ch: '!',
vector: [0.12, 0.12, 0.12, 0.12, 0.03, 0.08],
},
ShapeEntry {
ch: 'i',
vector: [0.08, 0.08, 0.12, 0.12, 0.12, 0.12],
},
ShapeEntry {
ch: '/',
vector: [0.03, 0.30, 0.18, 0.18, 0.30, 0.03],
},
ShapeEntry {
ch: '\\',
vector: [0.30, 0.03, 0.18, 0.18, 0.03, 0.30],
},
ShapeEntry {
ch: '(',
vector: [0.10, 0.00, 0.22, 0.00, 0.10, 0.00],
},
ShapeEntry {
ch: ')',
vector: [0.00, 0.10, 0.00, 0.22, 0.00, 0.10],
},
ShapeEntry {
ch: '[',
vector: [0.22, 0.00, 0.22, 0.00, 0.22, 0.00],
},
ShapeEntry {
ch: ']',
vector: [0.00, 0.22, 0.00, 0.22, 0.00, 0.22],
},
ShapeEntry {
ch: '{',
vector: [0.08, 0.00, 0.22, 0.00, 0.08, 0.00],
},
ShapeEntry {
ch: '}',
vector: [0.00, 0.08, 0.00, 0.22, 0.00, 0.08],
},
ShapeEntry {
ch: '<',
vector: [0.00, 0.18, 0.22, 0.00, 0.00, 0.18],
},
ShapeEntry {
ch: '>',
vector: [0.18, 0.00, 0.00, 0.22, 0.18, 0.00],
},
ShapeEntry {
ch: '^',
vector: [0.12, 0.12, 0.00, 0.00, 0.00, 0.00],
},
ShapeEntry {
ch: 'v',
vector: [0.00, 0.00, 0.00, 0.00, 0.12, 0.12],
},
ShapeEntry {
ch: 'T',
vector: [0.30, 0.30, 0.12, 0.12, 0.12, 0.12],
},
ShapeEntry {
ch: 'L',
vector: [0.15, 0.00, 0.15, 0.00, 0.25, 0.25],
},
ShapeEntry {
ch: 'J',
vector: [0.00, 0.15, 0.00, 0.15, 0.22, 0.22],
},
ShapeEntry {
ch: 'Y',
vector: [0.22, 0.22, 0.12, 0.12, 0.12, 0.12],
},
ShapeEntry {
ch: '+',
vector: [0.10, 0.10, 0.38, 0.38, 0.10, 0.10],
},
ShapeEntry {
ch: '*',
vector: [0.18, 0.18, 0.25, 0.25, 0.18, 0.18],
},
ShapeEntry {
ch: 'x',
vector: [0.25, 0.25, 0.10, 0.10, 0.25, 0.25],
},
ShapeEntry {
ch: 'o',
vector: [0.15, 0.15, 0.22, 0.22, 0.15, 0.15],
},
ShapeEntry {
ch: 'c',
vector: [0.12, 0.15, 0.18, 0.00, 0.12, 0.15],
},
ShapeEntry {
ch: 'n',
vector: [0.22, 0.22, 0.18, 0.18, 0.18, 0.18],
},
ShapeEntry {
ch: 'u',
vector: [0.18, 0.18, 0.18, 0.18, 0.22, 0.22],
},
ShapeEntry {
ch: 's',
vector: [0.08, 0.25, 0.18, 0.18, 0.25, 0.08],
},
ShapeEntry {
ch: 'r',
vector: [0.12, 0.18, 0.18, 0.05, 0.15, 0.00],
},
ShapeEntry {
ch: 'z',
vector: [0.20, 0.28, 0.18, 0.18, 0.28, 0.20],
},
ShapeEntry {
ch: 't',
vector: [0.12, 0.00, 0.25, 0.10, 0.12, 0.12],
},
ShapeEntry {
ch: '#',
vector: [0.55, 0.55, 0.60, 0.60, 0.55, 0.55],
},
ShapeEntry {
ch: '@',
vector: [0.58, 0.58, 0.65, 0.65, 0.55, 0.55],
},
ShapeEntry {
ch: '%',
vector: [0.40, 0.18, 0.25, 0.25, 0.18, 0.40],
},
ShapeEntry {
ch: '&',
vector: [0.30, 0.20, 0.40, 0.30, 0.35, 0.30],
},
ShapeEntry {
ch: '$',
vector: [0.20, 0.30, 0.35, 0.25, 0.30, 0.20],
},
ShapeEntry {
ch: 'H',
vector: [0.35, 0.35, 0.50, 0.50, 0.35, 0.35],
},
ShapeEntry {
ch: 'N',
vector: [0.42, 0.35, 0.38, 0.38, 0.35, 0.42],
},
ShapeEntry {
ch: 'M',
vector: [0.50, 0.50, 0.42, 0.42, 0.38, 0.38],
},
ShapeEntry {
ch: 'W',
vector: [0.38, 0.38, 0.50, 0.50, 0.35, 0.35],
},
ShapeEntry {
ch: 'O',
vector: [0.28, 0.28, 0.38, 0.38, 0.28, 0.28],
},
ShapeEntry {
ch: 'B',
vector: [0.48, 0.38, 0.48, 0.38, 0.48, 0.38],
},
ShapeEntry {
ch: 'D',
vector: [0.48, 0.32, 0.48, 0.38, 0.48, 0.32],
},
ShapeEntry {
ch: 'S',
vector: [0.15, 0.35, 0.30, 0.30, 0.35, 0.15],
},
];
#[inline]
fn enhance_contrast(v: &mut [f32; 6], exponent: f32) {
let max = v.iter().cloned().fold(0.0_f32, f32::max);
if max < 1e-6 {
return;
}
for val in v.iter_mut() {
let normalized = *val / max;
*val = normalized.powf(exponent) * max;
}
}
#[inline]
fn enhance_directional_contrast(v: &mut [f32; 6], neighbors: &[[f32; 6]], exponent: f32) {
let max = v.iter().cloned().fold(0.0_f32, f32::max);
if max < 1e-6 {
return;
}
for (i, val) in v.iter_mut().enumerate() {
let normalized = *val / max;
let max_neighbor = neighbors.iter().map(|n| n[i]).fold(0.0_f32, f32::max);
let neighbor_norm = if max > 1e-6 {
(max_neighbor / max).min(1.0)
} else {
0.0
};
let contrast_exp = if neighbor_norm > normalized + 0.05 {
exponent * 1.5
} else {
exponent
};
*val = normalized.powf(contrast_exp) * max;
}
}
pub fn map_shape_ascii(tl: f32, tr: f32, bl: f32, br: f32, contrast: f32) -> char {
let mut v = [tl, tr, (tl + bl) * 0.5, (tr + br) * 0.5, bl, br];
if contrast > 1.01 {
enhance_contrast(&mut v, contrast);
}
let max = v.iter().cloned().fold(0.0_f32, f32::max);
if max < 0.01 {
return ' ';
}
let inv_max = 1.0 / max;
for val in v.iter_mut() {
*val *= inv_max;
}
const EARLY_TERMINATION_THRESHOLD: f32 = 0.001;
let mut best_char = ' ';
let mut best_dist = f32::MAX;
for entry in &SHAPE_TABLE {
if entry.ch == ' ' {
continue;
}
let cv = &entry.vector;
let c_max = cv.iter().cloned().fold(0.0_f32, f32::max);
if c_max < 1e-6 {
continue;
}
let c_inv = 1.0 / c_max;
let mut dist = 0.0_f32;
for i in 0..6 {
let diff = v[i] - cv[i] * c_inv;
dist += diff * diff;
}
if dist < EARLY_TERMINATION_THRESHOLD {
best_char = entry.ch;
break;
}
if dist < best_dist {
best_dist = dist;
best_char = entry.ch;
}
}
best_char
}
pub fn map_shape_ascii_with_neighbors(
tl: f32,
tr: f32,
bl: f32,
br: f32,
neighbor_quads: &[[f32; 4]],
contrast: f32,
) -> char {
let mut v = [tl, tr, (tl + bl) * 0.5, (tr + br) * 0.5, bl, br];
let neighbor_vecs: Vec<[f32; 6]> = neighbor_quads
.iter()
.map(|q| {
[
q[0],
q[1],
(q[0] + q[2]) * 0.5,
(q[1] + q[3]) * 0.5,
q[2],
q[3],
]
})
.collect();
if contrast > 1.01 {
enhance_contrast(&mut v, contrast);
enhance_directional_contrast(&mut v, &neighbor_vecs, contrast);
}
let max = v.iter().cloned().fold(0.0_f32, f32::max);
if max < 0.01 {
return ' ';
}
let inv_max = 1.0 / max;
for val in v.iter_mut() {
*val *= inv_max;
}
const EARLY_TERMINATION_THRESHOLD: f32 = 0.001;
let mut best_char = ' ';
let mut best_dist = f32::MAX;
for entry in &SHAPE_TABLE {
if entry.ch == ' ' {
continue;
}
let cv = &entry.vector;
let c_max = cv.iter().cloned().fold(0.0_f32, f32::max);
if c_max < 1e-6 {
continue;
}
let c_inv = 1.0 / c_max;
let mut dist = 0.0_f32;
for i in 0..6 {
let diff = v[i] - cv[i] * c_inv;
dist += diff * diff;
}
if dist < EARLY_TERMINATION_THRESHOLD {
best_char = entry.ch;
break;
}
if dist < best_dist {
best_dist = dist;
best_char = entry.ch;
}
}
best_char
}
pub fn map_shape_braille(tl: f32, tr: f32, bl: f32, br: f32, threshold: f32) -> char {
let samples = [
tl, tr, tl * 0.67 + bl * 0.33, tr * 0.67 + br * 0.33, tl * 0.33 + bl * 0.67, tr * 0.33 + br * 0.67, bl, br, ];
let mut pattern: u8 = 0;
if samples[0] > threshold {
pattern |= 0x01;
} if samples[2] > threshold {
pattern |= 0x02;
} if samples[4] > threshold {
pattern |= 0x04;
} if samples[1] > threshold {
pattern |= 0x08;
} if samples[3] > threshold {
pattern |= 0x10;
} if samples[5] > threshold {
pattern |= 0x20;
} if samples[6] > threshold {
pattern |= 0x40;
} if samples[7] > threshold {
pattern |= 0x80;
}
char::from_u32(0x2800 + pattern as u32).unwrap_or(' ')
}
pub fn map_sculpted_outline(tl: f32, tr: f32, bl: f32, br: f32) -> char {
use crate::config_defaults::threshold;
let tl_on = tl > threshold::DEFAULT_SCULPTED_OUTLINE_THRESHOLD;
let tr_on = tr > threshold::DEFAULT_SCULPTED_OUTLINE_THRESHOLD;
let bl_on = bl > threshold::DEFAULT_SCULPTED_OUTLINE_THRESHOLD;
let br_on = br > threshold::DEFAULT_SCULPTED_OUTLINE_THRESHOLD;
let index =
(tl_on as u32) | ((tr_on as u32) << 1) | ((bl_on as u32) << 2) | ((br_on as u32) << 3);
match index {
0x0 => ' ',
0x1 => '\u{2598}', 0x2 => '\u{259D}', 0x3 => '\u{2580}', 0x4 => '\u{2596}', 0x5 => '\u{258C}', 0x6 => '\u{25E3}', 0x7 => '\u{25E4}', 0x8 => '\u{2597}', 0x9 => '\u{25E2}', 0xA => '\u{2590}', 0xB => '\u{25E5}', 0xC => '\u{2584}', 0xD => '\u{25E3}', 0xE => '\u{25E2}', 0xF => '\u{2588}', _ => ' ',
}
}
pub fn charset_level_count(charset: Charset) -> usize {
use crate::config_defaults::charset_levels;
match charset {
Charset::HalfBlock => charset_levels::HALF_BLOCK,
Charset::HalfBlockDual => 256, Charset::Ascii => charset_levels::ASCII,
Charset::Braille => charset_levels::BRAILLE,
Charset::Quadrant => charset_levels::QUADRANT, Charset::Shade => charset_levels::SHADE, Charset::Points => charset_levels::POINTS, Charset::Sculpted => charset_levels::SCULPTED, Charset::CustomAscii(ref chars) => chars.len().max(2), }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_charset_from_args_default() {
let args = Args {
ascii: false,
braille: false,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::HalfBlockDual);
}
#[test]
fn test_charset_from_args_ascii() {
let args = Args {
ascii: true,
braille: false,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Ascii);
}
#[test]
fn test_charset_from_args_braille() {
let args = Args {
ascii: false,
braille: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Braille);
}
#[test]
fn test_map_brightness_halfblock_min() {
assert_eq!(map_brightness(0.0, None, Charset::HalfBlock), ' ');
}
#[test]
fn test_map_brightness_halfblock_max() {
assert_eq!(map_brightness(1.0, None, Charset::HalfBlock), '\u{2588}');
}
#[test]
fn test_map_brightness_ascii_min() {
assert_eq!(map_brightness(0.0, None, Charset::Ascii), ' ');
}
#[test]
fn test_map_brightness_ascii_max() {
assert_eq!(map_brightness(1.0, None, Charset::Ascii), '@');
}
#[test]
fn test_map_brightness_braille_min() {
assert_eq!(map_brightness(0.0, None, Charset::Braille), '\u{2800}');
}
#[test]
fn test_map_brightness_braille_max() {
assert_eq!(map_brightness(1.0, None, Charset::Braille), '\u{280F}');
}
#[test]
fn test_map_brightness_clamped() {
assert_eq!(map_brightness(-0.5, None, Charset::HalfBlock), ' ');
assert_eq!(map_brightness(1.5, None, Charset::Ascii), '@');
}
#[test]
fn test_map_brightness_halfblock_mid() {
let char = map_brightness(0.5, None, Charset::HalfBlock);
assert_eq!(char, '\u{2584}');
}
#[test]
fn test_map_brightness_ascii_mid() {
let char = map_brightness(0.5, None, Charset::Ascii);
assert_eq!(char, '+');
}
#[test]
fn test_all_halfblock_chars() {
for (i, _) in HALF_BLOCK_CHARS.iter().enumerate() {
let brightness = i as f32 / (HALF_BLOCK_CHARS.len() - 1) as f32;
let char = map_brightness(brightness, None, Charset::HalfBlock);
assert_eq!(char, HALF_BLOCK_CHARS[i]);
}
}
#[test]
fn test_halfblock_chars_expanded() {
assert_eq!(HALF_BLOCK_CHARS.len(), 9);
assert_eq!(HALF_BLOCK_CHARS[0], ' ');
assert_eq!(HALF_BLOCK_CHARS[1], '\u{2581}');
assert_eq!(HALF_BLOCK_CHARS[4], '\u{2584}');
assert_eq!(HALF_BLOCK_CHARS[7], '\u{2587}');
assert_eq!(HALF_BLOCK_CHARS[8], '\u{2588}');
}
#[test]
fn test_map_vertical_block_empty() {
assert_eq!(map_vertical_block(0.0, 0.0), ' ');
assert_eq!(map_vertical_block(0.01, 0.01), ' ');
}
#[test]
fn test_map_vertical_block_top_only() {
assert_eq!(map_vertical_block(1.0, 0.0), '▀');
assert_eq!(map_vertical_block(0.5, 0.01), '▀');
assert_eq!(map_vertical_block(0.06, 0.0), '▀');
}
#[test]
fn test_map_vertical_block_bottom_only() {
assert_eq!(map_vertical_block(0.0, 1.0), '▄');
assert_eq!(map_vertical_block(0.01, 0.5), '▄');
assert_eq!(map_vertical_block(0.0, 0.06), '▄');
}
#[test]
fn test_map_vertical_block_full() {
assert_eq!(map_vertical_block(1.0, 1.0), '█');
assert_eq!(map_vertical_block(0.5, 0.5), '█');
assert_eq!(map_vertical_block(0.06, 0.06), '█');
}
#[test]
fn test_map_vertical_block_threshold_edge() {
assert_eq!(map_vertical_block(0.05, 0.0), ' ');
assert_eq!(map_vertical_block(0.06, 0.0), '▀');
assert_eq!(map_vertical_block(0.0, 0.05), ' ');
assert_eq!(map_vertical_block(0.0, 0.06), '▄');
}
#[test]
fn test_map_ascii_directional_top_min() {
assert_eq!(map_ascii_directional(0.0, true), ' ');
}
#[test]
fn test_map_ascii_directional_top_max() {
assert_eq!(map_ascii_directional(1.0, true), '^');
}
#[test]
fn test_map_ascii_directional_bottom_min() {
assert_eq!(map_ascii_directional(0.0, false), ' ');
}
#[test]
fn test_map_ascii_directional_bottom_max() {
assert_eq!(map_ascii_directional(1.0, false), 'v');
}
#[test]
fn test_map_ascii_directional_top_mid() {
assert_eq!(map_ascii_directional(0.5, true), '=');
}
#[test]
fn test_map_ascii_directional_bottom_mid() {
assert_eq!(map_ascii_directional(0.5, false), '=');
}
#[test]
fn test_map_ascii_directional_clamped() {
assert_eq!(map_ascii_directional(-0.5, true), ' ');
assert_eq!(map_ascii_directional(1.5, false), 'v');
}
#[test]
fn test_all_ascii_top_chars() {
for (i, expected) in ASCII_TOP_CHARS.iter().enumerate() {
let brightness = i as f32 / (ASCII_TOP_CHARS.len() - 1) as f32;
let char = map_ascii_directional(brightness, true);
assert_eq!(char, *expected);
}
}
#[test]
fn test_all_ascii_bottom_chars() {
for (i, expected) in ASCII_BOTTOM_CHARS.iter().enumerate() {
let brightness = i as f32 / (ASCII_BOTTOM_CHARS.len() - 1) as f32;
let char = map_ascii_directional(brightness, false);
assert_eq!(char, *expected);
}
}
#[test]
fn test_braille_subpixel_empty() {
assert_eq!(map_braille_subpixel(0.0, 0.0, 0.05), '\u{2800}');
}
#[test]
fn test_braille_subpixel_top_only() {
assert_eq!(map_braille_subpixel(1.0, 0.0, 0.05), '\u{2807}');
}
#[test]
fn test_braille_subpixel_bottom_only() {
assert_eq!(map_braille_subpixel(0.0, 1.0, 0.05), '\u{2838}');
}
#[test]
fn test_braille_subpixel_full() {
assert_eq!(map_braille_subpixel(1.0, 1.0, 0.05), '\u{283F}');
}
#[test]
fn test_braille_subpixel_top_levels() {
assert_eq!(map_braille_subpixel(0.0, 0.0, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(0.3, 0.0, 0.05), '\u{2803}');
assert_eq!(map_braille_subpixel(0.55, 0.0, 0.05), '\u{2807}');
assert_eq!(map_braille_subpixel(1.0, 0.0, 0.05), '\u{2807}');
}
#[test]
fn test_braille_subpixel_bottom_levels() {
assert_eq!(map_braille_subpixel(0.0, 0.0, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(0.0, 0.3, 0.05), '\u{2818}');
assert_eq!(map_braille_subpixel(0.0, 0.55, 0.05), '\u{2838}');
assert_eq!(map_braille_subpixel(0.0, 1.0, 0.05), '\u{2838}');
}
#[test]
fn test_braille_subpixel_all_combinations() {
assert_eq!(map_braille_subpixel(0.0, 0.0, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(0.0, 0.3, 0.05), '\u{2818}');
assert_eq!(map_braille_subpixel(0.0, 0.55, 0.05), '\u{2838}');
assert_eq!(map_braille_subpixel(0.0, 1.0, 0.05), '\u{2838}');
assert_eq!(map_braille_subpixel(0.3, 0.0, 0.05), '\u{2803}');
assert_eq!(map_braille_subpixel(0.3, 0.3, 0.05), '\u{281B}');
assert_eq!(map_braille_subpixel(0.3, 0.55, 0.05), '\u{283B}');
assert_eq!(map_braille_subpixel(0.3, 1.0, 0.05), '\u{283B}');
assert_eq!(map_braille_subpixel(0.55, 0.0, 0.05), '\u{2807}');
assert_eq!(map_braille_subpixel(0.55, 0.3, 0.05), '\u{281F}');
assert_eq!(map_braille_subpixel(0.55, 0.55, 0.05), '\u{283F}');
assert_eq!(map_braille_subpixel(0.55, 1.0, 0.05), '\u{283F}');
assert_eq!(map_braille_subpixel(1.0, 0.0, 0.05), '\u{2807}');
assert_eq!(map_braille_subpixel(1.0, 0.3, 0.05), '\u{281F}');
assert_eq!(map_braille_subpixel(1.0, 0.55, 0.05), '\u{283F}');
assert_eq!(map_braille_subpixel(1.0, 1.0, 0.05), '\u{283F}');
}
#[test]
fn test_braille_subpixel_threshold() {
assert_eq!(map_braille_subpixel(0.04, 0.04, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(0.06, 0.0, 0.05), '\u{2801}');
assert_eq!(map_braille_subpixel(0.0, 0.06, 0.05), '\u{2808}');
}
#[test]
fn test_braille_subpixel_clamping() {
assert_eq!(map_braille_subpixel(-0.5, 0.0, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(0.0, -0.5, 0.05), '\u{2800}');
assert_eq!(map_braille_subpixel(1.5, 1.5, 0.05), '\u{283F}');
}
#[test]
fn test_braille_dot_masks_values() {
assert_eq!(BRAILLE_DOT_MASKS[0], 0x00);
assert_eq!(BRAILLE_DOT_MASKS[1], 0x01);
assert_eq!(BRAILLE_DOT_MASKS[2], 0x03);
assert_eq!(BRAILLE_DOT_MASKS[3], 0x07);
assert_eq!(BRAILLE_DOT_MASKS[4], 0x07);
}
#[test]
fn test_map_brightness_braille_with_bottom() {
assert_eq!(map_brightness(1.0, Some(1.0), Charset::Braille), '\u{283F}');
assert_eq!(map_brightness(1.0, Some(0.0), Charset::Braille), '\u{2807}');
assert_eq!(map_brightness(0.0, Some(1.0), Charset::Braille), '\u{2838}');
assert_eq!(map_brightness(0.0, Some(0.0), Charset::Braille), '\u{2800}');
}
#[test]
fn test_map_quadrant_basic() {
assert_eq!(map_quadrant(0.0, 0.0, 0.0, 0.0, 0.05), ' ');
assert_eq!(map_quadrant(1.0, 0.0, 0.0, 0.0, 0.05), '\u{2598}');
assert_eq!(map_quadrant(0.0, 1.0, 0.0, 0.0, 0.05), '\u{259D}');
assert_eq!(map_quadrant(1.0, 1.0, 0.0, 0.0, 0.05), '\u{2580}');
assert_eq!(map_quadrant(1.0, 1.0, 1.0, 1.0, 0.05), '\u{2588}');
}
#[test]
fn test_map_quadrant_threshold() {
assert_eq!(map_quadrant(0.04, 0.04, 0.04, 0.04, 0.05), ' ');
assert_eq!(map_quadrant(0.06, 0.0, 0.0, 0.0, 0.05), '\u{2598}');
}
#[test]
fn test_charset_level_count_halfblock() {
assert_eq!(charset_level_count(Charset::HalfBlock), 9);
}
#[test]
fn test_charset_level_count_ascii() {
assert_eq!(charset_level_count(Charset::Ascii), 10);
}
#[test]
fn test_charset_from_args_quadrant() {
let args = Args {
quadrant: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Quadrant);
}
#[test]
fn test_charset_from_args_custom() {
let args = Args {
ascii_chars: Some("abc".to_string()),
..Default::default()
};
assert!(matches!(Charset::from_args(&args), Charset::CustomAscii(_)));
}
#[test]
fn test_from_custom_string_sorting() {
let charset = Charset::from_custom_string("@.");
if let Charset::CustomAscii(chars) = charset {
assert_eq!(chars[0], '.');
assert_eq!(chars[1], '@');
} else {
panic!("Expected CustomAscii");
}
}
#[test]
fn test_map_brightness_quadrant() {
assert_eq!(map_brightness(0.0, None, Charset::Quadrant), ' ');
assert_eq!(map_brightness(1.0, None, Charset::Quadrant), '\u{1FB0E}');
}
#[test]
fn test_map_brightness_custom() {
let charset = Charset::CustomAscii(vec!['a', 'b', 'c']);
assert_eq!(map_brightness(0.0, None, charset.clone()), 'a');
assert_eq!(map_brightness(1.0, None, charset.clone()), 'c');
assert_eq!(map_brightness(0.5, None, charset.clone()), 'b');
}
#[test]
fn test_charset_level_count_extended() {
assert_eq!(charset_level_count(Charset::Quadrant), 16);
assert_eq!(charset_level_count(Charset::CustomAscii(vec!['a', 'b'])), 2);
}
#[test]
fn test_map_shade_levels() {
assert_eq!(map_shade(0.0), ' ');
assert_eq!(map_shade(0.25), '░');
assert_eq!(map_shade(0.5), '▒');
assert_eq!(map_shade(0.75), '▓');
assert_eq!(map_shade(1.0), '█');
}
#[test]
fn test_map_shade_clamping() {
assert_eq!(map_shade(-0.5), ' ');
assert_eq!(map_shade(1.5), '█');
}
#[test]
fn test_map_shade_boundaries() {
assert_eq!(map_shade(0.1), ' '); assert_eq!(map_shade(0.15), '░'); assert_eq!(map_shade(0.4), '▒'); assert_eq!(map_shade(0.6), '▒'); assert_eq!(map_shade(0.7), '▓'); assert_eq!(map_shade(0.9), '█'); }
#[test]
fn test_map_point_threshold() {
assert_eq!(map_point(0.1, 0.15), ' ');
assert_eq!(map_point(0.2, 0.15), '▪');
assert_eq!(map_point(1.0, 0.15), '▪');
}
#[test]
fn test_map_point_at_threshold() {
assert_eq!(map_point(0.15, 0.15), ' ');
assert_eq!(map_point(0.151, 0.15), '▪');
}
#[test]
fn test_map_point_zero_threshold() {
assert_eq!(map_point(0.0, 0.0), ' ');
assert_eq!(map_point(0.001, 0.0), '▪');
}
#[test]
fn test_charset_from_args_shade() {
let args = Args {
shade: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Shade);
}
#[test]
fn test_charset_from_args_points() {
let args = Args {
points: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Points);
}
#[test]
fn test_charset_from_args_shade_priority() {
let args = Args {
shade: true,
braille: true,
ascii: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Shade);
}
#[test]
fn test_charset_from_args_quadrant_priority_over_shade() {
let args = Args {
quadrant: true,
shade: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::Quadrant);
}
#[test]
fn test_charset_level_count_shade() {
assert_eq!(charset_level_count(Charset::Shade), 5);
}
#[test]
fn test_charset_level_count_points() {
assert_eq!(charset_level_count(Charset::Points), 2);
}
#[test]
fn test_map_brightness_shade() {
assert_eq!(map_brightness(0.0, None, Charset::Shade), ' ');
assert_eq!(map_brightness(0.5, None, Charset::Shade), '▒');
assert_eq!(map_brightness(1.0, None, Charset::Shade), '█');
}
#[test]
fn test_map_brightness_shade_with_bottom() {
assert_eq!(map_brightness(1.0, Some(0.0), Charset::Shade), '▒'); assert_eq!(map_brightness(0.5, Some(0.5), Charset::Shade), '▒'); assert_eq!(map_brightness(1.0, Some(1.0), Charset::Shade), '█'); }
#[test]
fn test_map_brightness_points() {
assert_eq!(map_brightness(0.0, None, Charset::Points), ' ');
assert_eq!(map_brightness(0.1, None, Charset::Points), ' '); assert_eq!(map_brightness(0.2, None, Charset::Points), '▪'); assert_eq!(map_brightness(1.0, None, Charset::Points), '▪');
}
#[test]
fn test_map_brightness_points_with_bottom() {
assert_eq!(map_brightness(0.2, Some(0.1), Charset::Points), ' '); assert_eq!(map_brightness(0.3, Some(0.1), Charset::Points), '▪'); }
#[test]
fn test_half_block_dual_both_dark() {
assert_eq!(map_half_block_dual(0.0, 0.0, 0.05), ' ');
assert_eq!(map_half_block_dual(0.04, 0.04, 0.05), ' ');
}
#[test]
fn test_half_block_dual_top_only() {
assert_eq!(map_half_block_dual(0.5, 0.0, 0.05), '▀');
assert_eq!(map_half_block_dual(1.0, 0.03, 0.05), '▀');
}
#[test]
fn test_half_block_dual_bottom_only() {
assert_eq!(map_half_block_dual(0.0, 0.5, 0.05), '▄');
assert_eq!(map_half_block_dual(0.03, 1.0, 0.05), '▄');
}
#[test]
fn test_half_block_dual_both_bright() {
assert_eq!(map_half_block_dual(0.5, 0.5, 0.05), '▀');
assert_eq!(map_half_block_dual(1.0, 0.1, 0.05), '▀');
assert_eq!(map_half_block_dual(0.1, 1.0, 0.05), '▀');
}
#[test]
fn test_half_block_dual_threshold_edge() {
assert_eq!(map_half_block_dual(0.05, 0.0, 0.05), ' ');
assert_eq!(map_half_block_dual(0.051, 0.0, 0.05), '▀');
assert_eq!(map_half_block_dual(0.0, 0.05, 0.05), ' ');
assert_eq!(map_half_block_dual(0.0, 0.051, 0.05), '▄');
}
#[test]
fn test_charset_from_args_half_block_dual() {
let args = Args {
half_block_dual: true,
..Default::default()
};
assert_eq!(Charset::from_args(&args), Charset::HalfBlockDual);
}
#[test]
fn test_charset_level_count_half_block_dual() {
assert_eq!(charset_level_count(Charset::HalfBlockDual), 256);
}
#[test]
fn test_map_brightness_half_block_dual_uses_half_block_chars() {
assert_eq!(map_brightness(0.0, None, Charset::HalfBlockDual), ' ');
assert_eq!(
map_brightness(1.0, None, Charset::HalfBlockDual),
'\u{2588}'
);
}
#[test]
fn test_shape_ascii_empty_returns_space() {
assert_eq!(map_shape_ascii(0.0, 0.0, 0.0, 0.0, 1.5), ' ');
}
#[test]
fn test_shape_ascii_uniform_returns_dense_char() {
let ch = map_shape_ascii(0.8, 0.8, 0.8, 0.8, 1.0);
assert!(
"#@HMWOn*".contains(ch),
"Uniform brightness should produce a dense fill character, got '{ch}'"
);
}
#[test]
fn test_shape_ascii_diagonal_forward_slash() {
let ch = map_shape_ascii(0.0, 0.8, 0.8, 0.0, 1.0);
assert_eq!(
ch, '/',
"Top-right + bottom-left diagonal should produce '/'"
);
}
#[test]
fn test_shape_ascii_diagonal_backslash() {
let ch = map_shape_ascii(0.8, 0.0, 0.0, 0.8, 1.0);
assert_eq!(
ch, '\\',
"Top-left + bottom-right diagonal should produce '\\'"
);
}
#[test]
fn test_shape_ascii_top_heavy() {
let ch = map_shape_ascii(0.8, 0.8, 0.0, 0.0, 1.0);
assert!(
"^\"TY".contains(ch),
"Top-heavy pattern should produce a top-heavy character, got '{ch}'"
);
}
#[test]
fn test_shape_ascii_bottom_heavy() {
let ch = map_shape_ascii(0.0, 0.0, 0.8, 0.8, 1.0);
assert!(
"v_u;JL:".contains(ch),
"Bottom-heavy pattern should produce a bottom-leaning character, got '{ch}'"
);
}
#[test]
fn test_shape_ascii_left_heavy() {
let ch = map_shape_ascii(0.8, 0.0, 0.8, 0.0, 1.0);
assert!(
"[({BDLt".contains(ch),
"Left-heavy pattern should produce a left-side character, got '{ch}'"
);
}
#[test]
fn test_shape_ascii_right_heavy() {
let ch = map_shape_ascii(0.0, 0.8, 0.0, 0.8, 1.0);
assert!(
"])}.!i".contains(ch),
"Right-heavy pattern should produce a right-side character, got '{ch}'"
);
}
#[test]
fn test_shape_ascii_horizontal_band() {
let ch = map_shape_ascii(0.1, 0.1, 0.1, 0.1, 1.0);
assert_ne!(
ch, ' ',
"Low but nonzero uniform brightness should not be space"
);
}
#[test]
fn test_shape_ascii_contrast_enhancement() {
let ch_low = map_shape_ascii(0.0, 0.8, 0.8, 0.0, 1.0);
let ch_high = map_shape_ascii(0.0, 0.8, 0.8, 0.0, 3.0);
assert_ne!(ch_low, ' ');
assert_ne!(ch_high, ' ');
}
#[test]
fn test_shape_ascii_near_zero_not_space() {
let ch = map_shape_ascii(0.02, 0.0, 0.0, 0.0, 1.0);
assert_ne!(ch, ' ', "Brightness 0.02 should produce a character");
}
#[test]
fn test_shape_ascii_just_below_threshold() {
let ch = map_shape_ascii(0.005, 0.005, 0.005, 0.005, 1.0);
assert_eq!(ch, ' ', "Brightness below 0.01 threshold should be space");
}
#[test]
fn test_enhance_contrast_uniform_unchanged() {
let mut v = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5];
enhance_contrast(&mut v, 2.0);
for val in &v {
assert!(
(*val - 0.5).abs() < 1e-5,
"Uniform vector should be unchanged by contrast, got {val}"
);
}
}
#[test]
fn test_enhance_contrast_zero_unchanged() {
let mut v = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
enhance_contrast(&mut v, 2.0);
for val in &v {
assert!(val.abs() < 1e-6, "Zero vector should stay zero");
}
}
#[test]
fn test_enhance_contrast_amplifies_differences() {
let mut v = [1.0, 0.5, 0.0, 0.0, 0.0, 0.0];
enhance_contrast(&mut v, 2.0);
assert!((v[0] - 1.0).abs() < 1e-5, "Max value should stay at 1.0");
assert!(
(v[1] - 0.25).abs() < 1e-5,
"Half value should become 0.25 with exponent 2"
);
}
#[test]
fn test_shape_braille_empty() {
assert_eq!(map_shape_braille(0.0, 0.0, 0.0, 0.0, 0.05), '\u{2800}');
}
#[test]
fn test_shape_braille_full() {
assert_eq!(map_shape_braille(1.0, 1.0, 1.0, 1.0, 0.05), '\u{28FF}');
}
#[test]
fn test_shape_braille_top_only() {
let ch = map_shape_braille(1.0, 1.0, 0.0, 0.0, 0.05);
assert_eq!(ch, '\u{283F}'); }
#[test]
fn test_shape_braille_bottom_only() {
let ch = map_shape_braille(0.0, 0.0, 1.0, 1.0, 0.05);
assert_eq!(ch, char::from_u32(0x2800 + 0xF6).unwrap());
}
#[test]
fn test_shape_braille_left_only() {
let ch = map_shape_braille(1.0, 0.0, 1.0, 0.0, 0.05);
assert_eq!(ch, char::from_u32(0x2800 + 0x47).unwrap());
}
#[test]
fn test_shape_braille_right_only() {
let ch = map_shape_braille(0.0, 1.0, 0.0, 1.0, 0.05);
assert_eq!(ch, char::from_u32(0x2800 + 0xB8).unwrap());
}
#[test]
fn test_shape_braille_diagonal_tl_br() {
let ch = map_shape_braille(1.0, 0.0, 0.0, 1.0, 0.05);
assert_eq!(ch, char::from_u32(0x2800 + 0x07 + 0xB0).unwrap());
}
#[test]
fn test_shape_braille_threshold() {
assert_eq!(map_shape_braille(0.04, 0.04, 0.04, 0.04, 0.05), '\u{2800}');
assert_ne!(map_shape_braille(0.06, 0.06, 0.06, 0.06, 0.05), '\u{2800}');
}
#[test]
fn test_shape_braille_produces_more_patterns_than_old() {
let patterns: std::collections::HashSet<char> = [
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
(1.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 1.0),
(1.0, 0.0, 1.0, 0.0),
(0.0, 1.0, 0.0, 1.0),
(1.0, 0.0, 0.0, 1.0),
(0.0, 1.0, 1.0, 0.0),
(1.0, 1.0, 1.0, 1.0),
]
.iter()
.map(|(tl, tr, bl, br)| map_shape_braille(*tl, *tr, *bl, *br, 0.05))
.collect();
assert!(
patterns.len() >= 6,
"Expected at least 6 distinct braille patterns, got {}",
patterns.len()
);
}
#[test]
fn test_sculpted_outline_empty() {
assert_eq!(map_sculpted_outline(0.0, 0.0, 0.0, 0.0), ' ');
}
#[test]
fn test_sculpted_outline_full() {
assert_eq!(map_sculpted_outline(1.0, 1.0, 1.0, 1.0), '\u{2588}');
}
#[test]
fn test_sculpted_outline_top_half() {
let ch = map_sculpted_outline(0.9, 0.9, 0.0, 0.0);
assert_eq!(ch, '\u{2580}', "Top-heavy should produce ▀, got '{ch}'");
}
#[test]
fn test_sculpted_outline_top_half_dim() {
let ch = map_sculpted_outline(0.1, 0.1, 0.0, 0.0);
assert_eq!(ch, '\u{2580}', "Dim top should produce ▀, got '{ch}'");
}
#[test]
fn test_sculpted_outline_bottom_half() {
let ch = map_sculpted_outline(0.0, 0.0, 0.9, 0.9);
assert_eq!(ch, '\u{2584}', "Bottom half should produce ▄, got '{ch}'");
}
#[test]
fn test_sculpted_outline_left_half() {
let ch = map_sculpted_outline(0.9, 0.0, 0.9, 0.0);
assert_eq!(ch, '\u{258C}', "Left half should produce ▌, got '{ch}'");
}
#[test]
fn test_sculpted_outline_right_half() {
let ch = map_sculpted_outline(0.0, 0.9, 0.0, 0.9);
assert_eq!(ch, '\u{2590}', "Right-heavy should produce ▐, got '{ch}'");
}
#[test]
fn test_sculpted_outline_tl_quadrant() {
let ch = map_sculpted_outline(0.9, 0.0, 0.0, 0.0);
assert_eq!(ch, '\u{2598}', "Top-left only should produce ▘, got '{ch}'");
}
#[test]
fn test_sculpted_outline_br_quadrant() {
let ch = map_sculpted_outline(0.0, 0.0, 0.0, 0.9);
assert_eq!(
ch, '\u{2597}',
"Bottom-right only should produce ▗, got '{ch}'"
);
}
#[test]
fn test_sculpted_outline_diagonal_backslash() {
let ch = map_sculpted_outline(0.9, 0.0, 0.0, 0.9);
assert_eq!(
ch, '\u{25E2}',
"TL+BR diagonal should produce ◢, got '{ch}'"
);
}
#[test]
fn test_sculpted_outline_diagonal_forwardslash() {
let ch = map_sculpted_outline(0.0, 0.9, 0.9, 0.0);
assert_eq!(
ch, '\u{25E3}',
"TR+BL diagonal should produce ◣, got '{ch}'"
);
}
#[test]
fn test_sculpted_outline_three_quarter_tl_tr_bl() {
let ch = map_sculpted_outline(0.9, 0.9, 0.9, 0.0);
assert_eq!(ch, '\u{25E4}', "TL+TR+BL should produce ◤, got '{ch}'");
}
#[test]
fn test_sculpted_outline_three_quarter_tl_tr_br() {
let ch = map_sculpted_outline(0.9, 0.9, 0.0, 0.9);
assert_eq!(ch, '\u{25E5}', "TL+TR+BR should produce ◥, got '{ch}'");
}
#[test]
fn test_sculpted_outline_three_quarter_tl_bl_br() {
let ch = map_sculpted_outline(0.9, 0.0, 0.9, 0.9);
assert_eq!(ch, '\u{25E3}', "TL+BL+BR should produce ◣, got '{ch}'");
}
#[test]
fn test_sculpted_outline_three_quarter_tr_bl_br() {
let ch = map_sculpted_outline(0.0, 0.9, 0.9, 0.9);
assert_eq!(ch, '\u{25E2}', "TR+BL+BR should produce ◢, got '{ch}'");
}
#[test]
fn test_sculpted_outline_near_zero() {
assert_eq!(
map_sculpted_outline(0.005, 0.005, 0.005, 0.005),
' ',
"Below threshold should be space"
);
}
#[test]
fn test_sculpted_outline_produces_distinct_patterns() {
let patterns: std::collections::HashSet<char> = [
(0.9, 0.0, 0.0, 0.0), (0.0, 0.9, 0.0, 0.0), (0.0, 0.0, 0.9, 0.0), (0.0, 0.0, 0.0, 0.9), (0.9, 0.9, 0.0, 0.0), (0.0, 0.0, 0.9, 0.9), (0.9, 0.0, 0.9, 0.0), (0.0, 0.9, 0.0, 0.9), (0.9, 0.0, 0.0, 0.9), (0.0, 0.9, 0.9, 0.0), (0.9, 0.9, 0.9, 0.9), ]
.iter()
.map(|(tl, tr, bl, br)| map_sculpted_outline(*tl, *tr, *bl, *br))
.collect();
assert!(
patterns.len() >= 8,
"Expected at least 8 distinct outline patterns, got {}",
patterns.len()
);
}
#[test]
fn glyph_config_default_is_identity() {
let g = GlyphConfig::default();
assert_eq!(g.selection, None);
assert_eq!(
g.edge_threshold,
crate::config_defaults::glyph_consts::DEFAULT_GLYPH_EDGE_THRESHOLD
);
}
#[test]
fn sobel_flat_field_is_none() {
let n = [0.5_f32; 9];
assert_eq!(sobel_edge_glyph(&n, 0.05), None);
}
#[test]
fn sobel_below_threshold_is_none() {
let n = [0.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 0.0];
assert_eq!(sobel_edge_glyph(&n, 0.5), None);
}
#[test]
fn sobel_vertical_edge_is_pipe() {
let n = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
assert_eq!(sobel_edge_glyph(&n, 0.1), Some('|'));
}
#[test]
fn sobel_horizontal_edge_is_dash() {
let n = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
assert_eq!(sobel_edge_glyph(&n, 0.1), Some('-'));
}
#[test]
fn sobel_diagonal_edges() {
let down_right = [0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 0.0, 1.0, 1.0];
assert_eq!(sobel_edge_glyph(&down_right, 0.1), Some('/'));
let down_left = [0.0, 0.0, 0.0, 1.0, 0.5, 0.0, 1.0, 1.0, 0.0];
assert_eq!(sobel_edge_glyph(&down_left, 0.1), Some('\\'));
}
}