use crate::{BrailleGrid, DotmaxError};
pub const ASCII_DENSITY: &str =
" .'`^\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
pub const SIMPLE_DENSITY: &str = " .:-=+*#%@";
pub const BLOCKS_DENSITY: &str = " ░▒▓█";
pub const BRAILLE_DENSITY: &str = "⠀⠁⠃⠇⠏⠟⠿⡿⣿";
#[derive(Debug, Clone)]
pub struct DensitySet {
pub characters: Vec<char>,
pub name: String,
}
impl DensitySet {
pub fn new(name: String, characters: Vec<char>) -> Result<Self, DotmaxError> {
if characters.is_empty() {
return Err(DotmaxError::EmptyDensitySet);
}
if characters.len() > 256 {
return Err(DotmaxError::TooManyCharacters {
count: characters.len(),
});
}
Ok(Self { characters, name })
}
#[must_use]
pub fn map(&self, intensity: f32) -> char {
let clamped = intensity.clamp(0.0, 1.0);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let index = (clamped * (self.characters.len() - 1) as f32).round() as usize;
self.characters[index]
}
#[must_use]
pub fn ascii() -> Self {
Self {
characters: ASCII_DENSITY.chars().collect(),
name: "ASCII".to_string(),
}
}
#[must_use]
pub fn simple() -> Self {
Self {
characters: SIMPLE_DENSITY.chars().collect(),
name: "Simple".to_string(),
}
}
#[must_use]
pub fn blocks() -> Self {
Self {
characters: BLOCKS_DENSITY.chars().collect(),
name: "Blocks".to_string(),
}
}
#[must_use]
pub fn braille() -> Self {
Self {
characters: BRAILLE_DENSITY.chars().collect(),
name: "Braille".to_string(),
}
}
}
impl BrailleGrid {
pub fn render_density(
&mut self,
intensity_buffer: &[f32],
density_set: &DensitySet,
) -> Result<(), DotmaxError> {
let (width, height) = self.dimensions();
let expected_size = width * height;
if intensity_buffer.len() != expected_size {
return Err(DotmaxError::BufferSizeMismatch {
expected: expected_size,
actual: intensity_buffer.len(),
});
}
for (i, &intensity) in intensity_buffer.iter().enumerate() {
let character = density_set.map(intensity);
let cell_x = i % width;
let cell_y = i / width;
self.set_char(cell_x, cell_y, character)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_density_set_new_empty_returns_error() {
let result = DensitySet::new("Empty".to_string(), vec![]);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), DotmaxError::EmptyDensitySet));
}
#[test]
fn test_density_set_new_too_many_characters_returns_error() {
let too_many: Vec<char> = (0..300).map(|_| 'x').collect();
let result = DensitySet::new("TooMany".to_string(), too_many);
assert!(result.is_err());
match result.unwrap_err() {
DotmaxError::TooManyCharacters { count } => assert_eq!(count, 300),
_ => panic!("Expected TooManyCharacters error"),
}
}
#[test]
fn test_density_set_new_valid_range_succeeds() {
let result = DensitySet::new("Single".to_string(), vec!['x']);
assert!(result.is_ok());
assert_eq!(result.unwrap().characters.len(), 1);
let max_chars: Vec<char> = (0..256).map(|i| (i as u8) as char).collect();
let result = DensitySet::new("Max".to_string(), max_chars);
assert!(result.is_ok());
assert_eq!(result.unwrap().characters.len(), 256);
}
#[test]
fn test_density_set_map_boundary_values() {
let density = DensitySet::new("Test".to_string(), vec![' ', '.', ':', '@']).unwrap();
assert_eq!(density.map(0.0), ' ');
assert_eq!(density.map(1.0), '@');
}
#[test]
fn test_density_set_map_middle_value() {
let density = DensitySet::new("Test".to_string(), vec![' ', '.', ':', '@']).unwrap();
let mid_char = density.map(0.5);
assert!(mid_char == '.' || mid_char == ':');
}
#[test]
fn test_density_set_map_clamping() {
let density = DensitySet::new("Test".to_string(), vec![' ', '.', ':', '@']).unwrap();
assert_eq!(density.map(-0.5), ' ');
assert_eq!(density.map(-100.0), ' ');
assert_eq!(density.map(1.5), '@');
assert_eq!(density.map(100.0), '@');
assert_eq!(density.map(f32::NAN), ' ');
}
#[test]
fn test_predefined_ascii_density_set() {
let density = DensitySet::ascii();
assert_eq!(density.name, "ASCII");
assert_eq!(density.characters.len(), 69); assert_eq!(density.characters[0], ' ');
assert_eq!(density.characters[68], '$');
}
#[test]
fn test_predefined_simple_density_set() {
let density = DensitySet::simple();
assert_eq!(density.name, "Simple");
assert_eq!(density.characters.len(), 10);
assert_eq!(density.characters[0], ' ');
assert_eq!(density.characters[9], '@');
}
#[test]
fn test_predefined_blocks_density_set() {
let density = DensitySet::blocks();
assert_eq!(density.name, "Blocks");
assert_eq!(density.characters.len(), 5);
assert_eq!(density.characters[0], ' ');
assert_eq!(density.characters[4], '█');
}
#[test]
fn test_predefined_braille_density_set() {
let density = DensitySet::braille();
assert_eq!(density.name, "Braille");
assert_eq!(density.characters.len(), 9);
assert_eq!(density.characters[0], '⠀');
assert_eq!(density.characters[8], '⣿');
}
#[test]
fn test_render_density_buffer_size_mismatch() {
let mut grid = BrailleGrid::new(10, 5).unwrap();
let wrong_size = vec![0.5_f32; 30];
let density = DensitySet::simple();
let result = grid.render_density(&wrong_size, &density);
assert!(result.is_err());
match result.unwrap_err() {
DotmaxError::BufferSizeMismatch { expected, actual } => {
assert_eq!(expected, 50);
assert_eq!(actual, 30);
}
_ => panic!("Expected BufferSizeMismatch error"),
}
}
#[test]
fn test_render_density_valid_buffer_succeeds() {
let mut grid = BrailleGrid::new(10, 5).unwrap();
let valid_buffer = vec![0.5_f32; 50];
let density = DensitySet::simple();
let result = grid.render_density(&valid_buffer, &density);
assert!(result.is_ok());
}
#[test]
fn test_render_density_with_gradient() {
let mut grid = BrailleGrid::new(10, 5).unwrap();
let gradient: Vec<f32> = (0..50).map(|i| i as f32 / 49.0).collect();
let density = DensitySet::ascii();
let result = grid.render_density(&gradient, &density);
assert!(result.is_ok());
}
#[test]
fn test_render_density_actually_sets_characters() {
let mut grid = BrailleGrid::new(3, 2).unwrap();
let intensities = vec![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
let density = DensitySet::simple(); let result = grid.render_density(&intensities, &density);
assert!(result.is_ok());
assert_eq!(grid.get_char(0, 0), ' ');
let mid_char = grid.get_char(1, 0);
assert!(mid_char == '+' || mid_char == '*');
assert_eq!(grid.get_char(2, 0), '@');
assert_eq!(grid.get_char(0, 1), ' ');
assert_eq!(grid.get_char(2, 1), '@');
}
}