use crate::error::DotmaxError;
use tracing::{debug, error, info, instrument};
const MAX_GRID_WIDTH: usize = 10_000;
const MAX_GRID_HEIGHT: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
#[must_use]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[must_use]
pub const fn black() -> Self {
Self { r: 0, g: 0, b: 0 }
}
#[must_use]
pub const fn white() -> Self {
Self {
r: 255,
g: 255,
b: 255,
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrailleDot {
Dot1 = 0b0000_0001,
Dot2 = 0b0000_0010,
Dot3 = 0b0000_0100,
Dot4 = 0b0000_1000,
Dot5 = 0b0001_0000,
Dot6 = 0b0010_0000,
Dot7 = 0b0100_0000,
Dot8 = 0b1000_0000,
}
#[inline]
#[must_use]
pub fn dots_to_char(dots: u8) -> char {
char::from_u32(0x2800 + u32::from(dots)).unwrap_or('⠀')
}
#[derive(Debug, Clone)]
pub struct BrailleGrid {
width: usize,
height: usize,
patterns: Vec<u8>,
colors: Vec<Option<Color>>,
characters: Vec<Option<char>>,
}
impl BrailleGrid {
#[instrument]
pub fn new(width: usize, height: usize) -> Result<Self, DotmaxError> {
if width == 0 || height == 0 {
error!(
width = width,
height = height,
"Invalid grid dimensions: width or height is zero"
);
return Err(DotmaxError::InvalidDimensions { width, height });
}
if width > MAX_GRID_WIDTH || height > MAX_GRID_HEIGHT {
error!(
width = width,
height = height,
max_width = MAX_GRID_WIDTH,
max_height = MAX_GRID_HEIGHT,
"Invalid grid dimensions: exceeds maximum allowed size"
);
return Err(DotmaxError::InvalidDimensions { width, height });
}
let size = width * height;
info!(
width = width,
height = height,
total_cells = size,
"Creating BrailleGrid"
);
Ok(Self {
width,
height,
patterns: vec![0; size],
colors: vec![None; size],
characters: vec![None; size], })
}
#[must_use]
pub const fn width(&self) -> usize {
self.width
}
#[must_use]
pub const fn height(&self) -> usize {
self.height
}
#[must_use]
pub const fn dot_width(&self) -> usize {
self.width * 2
}
#[must_use]
pub const fn dot_height(&self) -> usize {
self.height * 4
}
#[must_use]
pub const fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
#[instrument(skip(self))]
pub fn clear(&mut self) {
debug!(
width = self.width,
height = self.height,
"Clearing all dots in grid"
);
self.patterns.fill(0);
self.colors.fill(None);
}
pub fn set_dot(&mut self, dot_x: usize, dot_y: usize) -> Result<(), DotmaxError> {
if dot_x >= self.dot_width() || dot_y >= self.dot_height() {
error!(
dot_x = dot_x,
dot_y = dot_y,
dot_width = self.dot_width(),
dot_height = self.dot_height(),
"Out of bounds dot access: ({}, {}) in grid of size ({}, {})",
dot_x,
dot_y,
self.dot_width(),
self.dot_height()
);
return Err(DotmaxError::OutOfBounds {
x: dot_x,
y: dot_y,
width: self.dot_width(),
height: self.dot_height(),
});
}
let cell_x = dot_x / 2;
let cell_y = dot_y / 4;
let cell_index = cell_y * self.width + cell_x;
let local_x = dot_x % 2;
let local_y = dot_y % 4;
let dot_bit = match (local_x, local_y) {
(0, 0) => BrailleDot::Dot1 as u8,
(0, 1) => BrailleDot::Dot2 as u8,
(0, 2) => BrailleDot::Dot3 as u8,
(0, 3) => BrailleDot::Dot7 as u8,
(1, 0) => BrailleDot::Dot4 as u8,
(1, 1) => BrailleDot::Dot5 as u8,
(1, 2) => BrailleDot::Dot6 as u8,
(1, 3) => BrailleDot::Dot8 as u8,
_ => unreachable!(),
};
self.patterns[cell_index] |= dot_bit;
Ok(())
}
pub fn get_dot(&self, x: usize, y: usize, dot_index: u8) -> Result<bool, DotmaxError> {
if x >= self.width || y >= self.height {
return Err(DotmaxError::OutOfBounds {
x,
y,
width: self.width,
height: self.height,
});
}
if dot_index > 7 {
return Err(DotmaxError::InvalidDotIndex { index: dot_index });
}
let cell_index = y * self.width + x;
let pattern = self.patterns[cell_index];
let dot_bit = 1u8 << dot_index;
Ok((pattern & dot_bit) != 0)
}
pub fn clear_region(
&mut self,
x: usize,
y: usize,
width: usize,
height: usize,
) -> Result<(), DotmaxError> {
let end_x = x.saturating_add(width);
let end_y = y.saturating_add(height);
if end_x > self.width || end_y > self.height {
return Err(DotmaxError::OutOfBounds {
x: end_x.saturating_sub(1),
y: end_y.saturating_sub(1),
width: self.width,
height: self.height,
});
}
for row_idx in y..end_y {
for col_idx in x..end_x {
let cell_index = row_idx * self.width + col_idx;
self.patterns[cell_index] = 0;
self.colors[cell_index] = None;
}
}
Ok(())
}
#[must_use]
pub fn get_char(&self, cell_x: usize, cell_y: usize) -> char {
if cell_x >= self.width || cell_y >= self.height {
return '⠀';
}
let index = cell_y * self.width + cell_x;
if let Some(ch) = self.characters[index] {
return ch;
}
dots_to_char(self.patterns[index])
}
#[must_use]
pub fn get_color(&self, cell_x: usize, cell_y: usize) -> Option<Color> {
if cell_x >= self.width || cell_y >= self.height {
return None;
}
let index = cell_y * self.width + cell_x;
self.colors[index]
}
#[must_use]
pub fn is_empty(&self, cell_x: usize, cell_y: usize) -> bool {
if cell_x >= self.width || cell_y >= self.height {
return true;
}
let index = cell_y * self.width + cell_x;
self.patterns[index] == 0
}
#[must_use]
pub fn get_raw_patterns(&self) -> &[u8] {
&self.patterns
}
pub fn set_raw_patterns(&mut self, data: &[u8]) {
let copy_len = data.len().min(self.patterns.len());
self.patterns[..copy_len].copy_from_slice(&data[..copy_len]);
}
#[must_use]
pub fn to_unicode_grid(&self) -> Vec<Vec<char>> {
let mut result = Vec::with_capacity(self.height);
for y in 0..self.height {
let mut row = Vec::with_capacity(self.width);
for x in 0..self.width {
let index = y * self.width + x;
row.push(dots_to_char(self.patterns[index]));
}
result.push(row);
}
result
}
pub fn cell_to_braille_char(&self, x: usize, y: usize) -> Result<char, DotmaxError> {
if x >= self.width || y >= self.height {
return Err(DotmaxError::OutOfBounds {
x,
y,
width: self.width,
height: self.height,
});
}
let index = y * self.width + x;
Ok(dots_to_char(self.patterns[index]))
}
#[instrument(skip(self))]
pub fn resize(&mut self, new_width: usize, new_height: usize) -> Result<(), DotmaxError> {
debug!(
old_width = self.width,
old_height = self.height,
new_width = new_width,
new_height = new_height,
"Resizing BrailleGrid"
);
if new_width == 0 || new_height == 0 {
error!(
new_width = new_width,
new_height = new_height,
"Invalid resize dimensions: width or height is zero"
);
return Err(DotmaxError::InvalidDimensions {
width: new_width,
height: new_height,
});
}
if new_width > MAX_GRID_WIDTH || new_height > MAX_GRID_HEIGHT {
error!(
new_width = new_width,
new_height = new_height,
max_width = MAX_GRID_WIDTH,
max_height = MAX_GRID_HEIGHT,
"Invalid resize dimensions: exceeds maximum allowed size"
);
return Err(DotmaxError::InvalidDimensions {
width: new_width,
height: new_height,
});
}
let new_size = new_width * new_height;
let mut new_patterns = vec![0; new_size];
let mut new_colors = vec![None; new_size];
let copy_width = self.width.min(new_width);
let copy_height = self.height.min(new_height);
for y in 0..copy_height {
for x in 0..copy_width {
let old_index = y * self.width + x;
let new_index = y * new_width + x;
new_patterns[new_index] = self.patterns[old_index];
new_colors[new_index] = self.colors[old_index];
}
}
self.width = new_width;
self.height = new_height;
self.patterns = new_patterns;
self.colors = new_colors;
Ok(())
}
#[instrument(skip(self))]
pub fn enable_color_support(&mut self) {
debug!(
width = self.width,
height = self.height,
"Enabling color support (already enabled in current implementation)"
);
}
pub fn set_cell_color(&mut self, x: usize, y: usize, color: Color) -> Result<(), DotmaxError> {
if x >= self.width || y >= self.height {
error!(
x = x,
y = y,
width = self.width,
height = self.height,
"Out of bounds color assignment: ({}, {}) in grid of size ({}, {})",
x,
y,
self.width,
self.height
);
return Err(DotmaxError::OutOfBounds {
x,
y,
width: self.width,
height: self.height,
});
}
let index = y * self.width + x;
self.colors[index] = Some(color);
Ok(())
}
pub fn clear_colors(&mut self) {
self.colors.fill(None);
}
pub fn set_char(&mut self, x: usize, y: usize, character: char) -> Result<(), DotmaxError> {
if x >= self.width || y >= self.height {
error!(
x = x,
y = y,
width = self.width,
height = self.height,
"Out of bounds character assignment: ({}, {}) in grid of size ({}, {})",
x,
y,
self.width,
self.height
);
return Err(DotmaxError::OutOfBounds {
x,
y,
width: self.width,
height: self.height,
});
}
let index = y * self.width + x;
self.characters[index] = Some(character);
Ok(())
}
pub fn clear_characters(&mut self) {
self.characters.fill(None);
}
pub fn apply_color_scheme(
&mut self,
intensities: &[f32],
scheme: &crate::color::schemes::ColorScheme,
) -> Result<(), DotmaxError> {
let expected_len = self.width * self.height;
if intensities.len() != expected_len {
return Err(DotmaxError::BufferSizeMismatch {
expected: expected_len,
actual: intensities.len(),
});
}
for (index, &intensity) in intensities.iter().enumerate() {
let normalized = if intensity.is_nan() {
0.0
} else if intensity.is_infinite() {
if intensity.is_sign_positive() {
1.0
} else {
0.0
}
} else {
intensity.clamp(0.0, 1.0)
};
self.colors[index] = Some(scheme.sample(normalized));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid_dimensions() {
let grid = BrailleGrid::new(80, 24);
assert!(grid.is_ok());
let grid = grid.unwrap();
assert_eq!(grid.dimensions(), (80, 24));
assert_eq!(grid.width(), 80);
assert_eq!(grid.height(), 24);
}
#[test]
fn test_braille_grid_creation() {
let grid = BrailleGrid::new(40, 20).unwrap();
assert_eq!(grid.width(), 40);
assert_eq!(grid.height(), 20);
assert_eq!(grid.dot_width(), 80);
assert_eq!(grid.dot_height(), 80);
}
#[test]
fn test_new_minimal_dimensions() {
let grid = BrailleGrid::new(1, 1);
assert!(grid.is_ok());
assert_eq!(grid.unwrap().dimensions(), (1, 1));
}
#[test]
fn test_new_large_dimensions() {
let grid = BrailleGrid::new(200, 50);
assert!(grid.is_ok());
assert_eq!(grid.unwrap().dimensions(), (200, 50));
}
#[test]
fn test_new_max_dimensions() {
let grid = BrailleGrid::new(10_000, 10_000);
assert!(grid.is_ok());
}
#[test]
fn test_new_zero_width_returns_error() {
let result = BrailleGrid::new(0, 24);
assert!(matches!(
result,
Err(DotmaxError::InvalidDimensions {
width: 0,
height: 24
})
));
}
#[test]
fn test_new_zero_height_returns_error() {
let result = BrailleGrid::new(80, 0);
assert!(matches!(
result,
Err(DotmaxError::InvalidDimensions {
width: 80,
height: 0
})
));
}
#[test]
fn test_new_both_zero_returns_error() {
let result = BrailleGrid::new(0, 0);
assert!(matches!(
result,
Err(DotmaxError::InvalidDimensions {
width: 0,
height: 0
})
));
}
#[test]
fn test_new_exceeds_max_width() {
let result = BrailleGrid::new(10_001, 100);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_new_exceeds_max_height() {
let result = BrailleGrid::new(100, 10_001);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_set_dot() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(0, 0).unwrap();
assert_eq!(grid.get_char(0, 0), '⠁');
grid.set_dot(1, 0).unwrap();
assert_eq!(grid.get_char(0, 0), '⠉');
}
#[test]
fn test_dot_positions() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.clear();
grid.set_dot(0, 0).unwrap(); assert_eq!(grid.patterns[0], 0b0000_0001);
grid.clear();
grid.set_dot(0, 1).unwrap(); assert_eq!(grid.patterns[0], 0b0000_0010);
grid.clear();
grid.set_dot(0, 2).unwrap(); assert_eq!(grid.patterns[0], 0b0000_0100);
grid.clear();
grid.set_dot(0, 3).unwrap(); assert_eq!(grid.patterns[0], 0b0100_0000);
grid.clear();
grid.set_dot(1, 0).unwrap(); assert_eq!(grid.patterns[0], 0b0000_1000);
grid.clear();
grid.set_dot(1, 1).unwrap(); assert_eq!(grid.patterns[0], 0b0001_0000);
grid.clear();
grid.set_dot(1, 2).unwrap(); assert_eq!(grid.patterns[0], 0b0010_0000);
grid.clear();
grid.set_dot(1, 3).unwrap(); assert_eq!(grid.patterns[0], 0b1000_0000);
}
#[test]
fn test_get_dot_all_positions() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
for dot_y in 0..4 {
for dot_x in 0..2 {
grid.set_dot(5 * 2 + dot_x, 5 * 4 + dot_y).unwrap();
}
}
for dot_index in 0..8 {
assert!(
grid.get_dot(5, 5, dot_index).unwrap(),
"Dot {dot_index} should be set"
);
}
}
#[test]
fn test_set_dot_out_of_bounds() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.set_dot(100, 5);
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_get_dot_out_of_bounds() {
let grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.get_dot(100, 100, 0);
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_get_dot_invalid_dot_index() {
let grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.get_dot(5, 5, 8);
assert!(matches!(result, Err(DotmaxError::InvalidDotIndex { .. })));
}
#[test]
fn test_bounds_checking() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.set_dot(1000, 1000);
assert!(result.is_err());
assert_eq!(grid.get_char(1000, 1000), '⠀');
assert!(grid.is_empty(1000, 1000));
}
#[test]
fn test_clear() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(0, 0).unwrap();
grid.set_dot(5, 5).unwrap();
grid.clear();
assert_eq!(grid.get_char(0, 0), '⠀');
assert!(grid.is_empty(0, 0));
}
#[test]
fn test_clear_empty_grid() {
let mut grid = BrailleGrid::new(5, 5).unwrap();
grid.clear(); assert!(grid.is_empty(0, 0));
}
#[test]
fn test_clear_region_basic() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
grid.set_dot(5 * 2, 5 * 4).unwrap(); grid.set_dot(6 * 2, 6 * 4).unwrap(); grid.set_dot(10 * 2, 10 * 4).unwrap();
grid.clear_region(5, 5, 2, 2).unwrap();
assert!(grid.is_empty(5, 5));
assert!(grid.is_empty(6, 6));
assert!(!grid.is_empty(10, 10));
}
#[test]
fn test_clear_region_out_of_bounds() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.clear_region(5, 5, 10, 10);
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_clear_region_zero_size() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.clear_region(5, 5, 0, 0);
assert!(result.is_ok());
}
#[test]
fn test_dimensions_returns_correct_size() {
let grid1 = BrailleGrid::new(80, 24).unwrap();
assert_eq!(grid1.dimensions(), (80, 24));
let grid2 = BrailleGrid::new(100, 50).unwrap();
assert_eq!(grid2.dimensions(), (100, 50));
let grid3 = BrailleGrid::new(1, 1).unwrap();
assert_eq!(grid3.dimensions(), (1, 1));
}
#[test]
fn test_dots_to_char() {
assert_eq!(dots_to_char(0b0000_0000), '⠀');
assert_eq!(dots_to_char(0b1111_1111), '⣿');
assert_eq!(dots_to_char(0b0000_0001), '⠁'); assert_eq!(dots_to_char(0b0000_1000), '⠈'); }
#[test]
fn test_color_rgb() {
let color = Color::rgb(255, 128, 64);
assert_eq!(color.r, 255);
assert_eq!(color.g, 128);
assert_eq!(color.b, 64);
}
#[test]
fn test_color_black() {
let color = Color::black();
assert_eq!(color.r, 0);
assert_eq!(color.g, 0);
assert_eq!(color.b, 0);
}
#[test]
fn test_color_white() {
let color = Color::white();
assert_eq!(color.r, 255);
assert_eq!(color.g, 255);
assert_eq!(color.b, 255);
}
#[test]
fn test_color_equality() {
let color1 = Color::rgb(100, 150, 200);
let color2 = Color::rgb(100, 150, 200);
let color3 = Color::rgb(100, 150, 201);
assert_eq!(color1, color2);
assert_ne!(color1, color3);
}
#[test]
fn test_all_256_braille_patterns() {
for bitfield in 0u8..=255 {
let ch = dots_to_char(bitfield);
let expected = char::from_u32(0x2800 + u32::from(bitfield)).unwrap();
assert_eq!(
ch, expected,
"Failed for bitfield {bitfield:08b} (decimal {bitfield})"
);
}
}
#[test]
fn test_empty_cell_is_u2800() {
let ch = dots_to_char(0b0000_0000);
assert_eq!(ch, '\u{2800}', "Empty cell should be blank braille U+2800");
}
#[test]
fn test_full_cell_is_u28ff() {
let ch = dots_to_char(0b1111_1111);
assert_eq!(ch, '\u{28FF}', "Full cell should be U+28FF (all dots)");
}
#[test]
fn test_specific_braille_patterns() {
assert_eq!(dots_to_char(0b0000_0101), '\u{2805}');
assert_eq!(dots_to_char(0b0000_1111), '\u{280F}');
assert_eq!(dots_to_char(0b0000_0001), '\u{2801}'); assert_eq!(dots_to_char(0b0000_1000), '\u{2808}'); assert_eq!(dots_to_char(0b0100_0000), '\u{2840}'); assert_eq!(dots_to_char(0b1000_0000), '\u{2880}'); }
#[test]
fn test_to_unicode_grid_dimensions() {
let grid = BrailleGrid::new(5, 5).unwrap();
let chars = grid.to_unicode_grid();
assert_eq!(chars.len(), 5, "Grid should have 5 rows");
for row in &chars {
assert_eq!(row.len(), 5, "Each row should have 5 columns");
}
}
#[test]
fn test_to_unicode_grid_various_sizes() {
let grid1 = BrailleGrid::new(80, 24).unwrap();
let chars1 = grid1.to_unicode_grid();
assert_eq!(chars1.len(), 24);
assert_eq!(chars1[0].len(), 80);
let grid2 = BrailleGrid::new(1, 1).unwrap();
let chars2 = grid2.to_unicode_grid();
assert_eq!(chars2.len(), 1);
assert_eq!(chars2[0].len(), 1);
let grid3 = BrailleGrid::new(100, 50).unwrap();
let chars3 = grid3.to_unicode_grid();
assert_eq!(chars3.len(), 50);
assert_eq!(chars3[0].len(), 100);
}
#[test]
fn test_to_unicode_grid_empty() {
let grid = BrailleGrid::new(3, 3).unwrap();
let chars = grid.to_unicode_grid();
for row in chars {
for ch in row {
assert_eq!(ch, '\u{2800}', "Empty grid should have blank braille");
}
}
}
#[test]
fn test_to_unicode_grid_with_dots() {
let mut grid = BrailleGrid::new(5, 5).unwrap();
grid.set_dot(0, 0).unwrap();
for dot_y in 0..4 {
for dot_x in 0..2 {
grid.set_dot(2 * 2 + dot_x, 2 * 4 + dot_y).unwrap();
}
}
let chars = grid.to_unicode_grid();
assert_eq!(chars[0][0], '\u{2801}');
assert_eq!(chars[2][2], '\u{28FF}');
assert_eq!(chars[1][1], '\u{2800}');
assert_eq!(chars[4][4], '\u{2800}');
}
#[test]
fn test_cell_to_braille_char_out_of_bounds() {
let grid = BrailleGrid::new(10, 10).unwrap();
let result1 = grid.cell_to_braille_char(100, 5);
assert!(matches!(result1, Err(DotmaxError::OutOfBounds { .. })));
let result2 = grid.cell_to_braille_char(5, 100);
assert!(matches!(result2, Err(DotmaxError::OutOfBounds { .. })));
let result3 = grid.cell_to_braille_char(10, 10); assert!(matches!(result3, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_cell_to_braille_char_correct_conversion() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(5 * 2, 5 * 4).unwrap(); grid.set_dot(5 * 2, 5 * 4 + 1).unwrap(); grid.set_dot(5 * 2, 5 * 4 + 2).unwrap(); grid.set_dot(5 * 2 + 1, 5 * 4).unwrap();
let ch = grid.cell_to_braille_char(5, 5).unwrap();
assert_eq!(ch, '\u{280F}');
}
#[test]
fn test_cell_to_braille_char_empty_cells() {
let grid = BrailleGrid::new(10, 10).unwrap();
for y in 0..10 {
for x in 0..10 {
let ch = grid.cell_to_braille_char(x, y).unwrap();
assert_eq!(ch, '\u{2800}', "Empty cell ({x}, {y}) should be blank");
}
}
}
#[test]
fn test_unicode_conversion_after_clear() {
let mut grid = BrailleGrid::new(5, 5).unwrap();
grid.set_dot(0, 0).unwrap();
grid.set_dot(5, 5).unwrap();
assert_ne!(grid.cell_to_braille_char(0, 0).unwrap(), '\u{2800}');
grid.clear();
let chars = grid.to_unicode_grid();
for row in chars {
for ch in row {
assert_eq!(ch, '\u{2800}');
}
}
}
#[test]
fn test_unicode_range_validity() {
let grid = BrailleGrid::new(5, 5).unwrap();
let chars = grid.to_unicode_grid();
for row in chars {
for ch in row {
assert!(
('\u{2800}'..='\u{28FF}').contains(&ch),
"Character {ch} is outside braille range U+2800-U+28FF"
);
}
}
}
#[test]
fn test_invalid_dimensions_error_message_includes_context() {
let result = BrailleGrid::new(0, 10);
match result {
Err(DotmaxError::InvalidDimensions { width, height }) => {
let msg = format!("{}", DotmaxError::InvalidDimensions { width, height });
assert!(msg.contains('0'), "Error message should include width=0");
assert!(msg.contains("10"), "Error message should include height=10");
assert!(
msg.contains("width") && msg.contains("height"),
"Error message should label dimensions"
);
}
_ => panic!("Expected InvalidDimensions error"),
}
}
#[test]
fn test_out_of_bounds_error_message_includes_all_context() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.set_dot(100, 50);
match result {
Err(DotmaxError::OutOfBounds {
x,
y,
width,
height,
}) => {
let msg = format!(
"{}",
DotmaxError::OutOfBounds {
x,
y,
width,
height
}
);
assert!(msg.contains("100"), "Error message should include x=100");
assert!(msg.contains("50"), "Error message should include y=50");
assert!(
msg.contains("20"),
"Error message should include dot_width=20"
);
assert!(
msg.contains("40"),
"Error message should include dot_height=40"
);
}
_ => panic!("Expected OutOfBounds error"),
}
}
#[test]
fn test_invalid_dot_index_error_message_includes_index() {
let grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.get_dot(5, 5, 10);
match result {
Err(DotmaxError::InvalidDotIndex { index }) => {
let msg = format!("{}", DotmaxError::InvalidDotIndex { index });
assert!(msg.contains("10"), "Error message should include index=10");
assert!(
msg.contains("0-7"),
"Error message should specify valid range"
);
}
_ => panic!("Expected InvalidDotIndex error"),
}
}
#[test]
fn test_new_exceeds_both_max_dimensions() {
let result = BrailleGrid::new(20_000, 20_000);
assert!(
matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
"Grid exceeding MAX_GRID_WIDTH and MAX_GRID_HEIGHT should return InvalidDimensions"
);
}
#[test]
fn test_set_dot_invalid_dot_index_high() {
let grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.get_dot(5, 5, 255);
assert!(
matches!(result, Err(DotmaxError::InvalidDotIndex { index: 255 })),
"Dot index 255 should return InvalidDotIndex error"
);
}
#[test]
fn test_resize_grow_updates_dimensions() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.resize(20, 20).unwrap();
assert_eq!(grid.dimensions(), (20, 20));
assert_eq!(grid.width(), 20);
assert_eq!(grid.height(), 20);
}
#[test]
fn test_resize_grow_preserves_existing_dots() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(5, 5).unwrap(); grid.set_dot(18, 38).unwrap();
let cell_2_1 = 10 + 2; let cell_9_9 = 9 * 10 + 9; assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
assert_ne!(grid.patterns[cell_9_9], 0, "Cell (9,9) should have dots");
grid.resize(20, 20).unwrap();
let new_cell_2_1 = 20 + 2; let new_cell_9_9 = 9 * 20 + 9;
assert_ne!(
grid.patterns[new_cell_2_1], 0,
"Cell (2,1) dots should be preserved"
);
assert_ne!(
grid.patterns[new_cell_9_9], 0,
"Cell (9,9) dots should be preserved"
);
let new_cell_15_15 = 15 * 20 + 15;
assert_eq!(
grid.patterns[new_cell_15_15], 0,
"New cells should be empty"
);
}
#[test]
fn test_resize_shrink_truncates_cleanly() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
grid.set_dot(5, 5).unwrap(); grid.set_dot(30, 60).unwrap();
let cell_2_1 = 20 + 2;
let cell_15_15 = 15 * 20 + 15;
assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
assert_ne!(
grid.patterns[cell_15_15], 0,
"Cell (15,15) should have dots"
);
grid.resize(10, 10).unwrap();
assert_eq!(grid.dimensions(), (10, 10));
let new_cell_2_1 = 10 + 2;
assert_ne!(
grid.patterns[new_cell_2_1], 0,
"Cell (2,1) dots should be preserved"
);
assert_eq!(
grid.patterns.len(),
100,
"Grid should have only 100 cells after resize to 10×10"
);
}
#[test]
fn test_resize_same_dimensions() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(5, 5).unwrap();
let cell_2_1 = 10 + 2;
let original_pattern = grid.patterns[cell_2_1];
assert_ne!(original_pattern, 0);
grid.resize(10, 10).unwrap();
assert_eq!(grid.dimensions(), (10, 10));
assert_eq!(
grid.patterns[cell_2_1], original_pattern,
"Existing dot pattern should be preserved"
);
}
#[test]
fn test_resize_with_colors_syncs_color_buffer() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.resize(20, 20).unwrap();
assert_eq!(
grid.colors.len(),
400,
"Color buffer should have 400 cells for 20×20 grid"
);
}
#[test]
fn test_resize_zero_width_error() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.resize(0, 10);
assert!(
matches!(
result,
Err(DotmaxError::InvalidDimensions {
width: 0,
height: 10
})
),
"Resize to width=0 should return InvalidDimensions error"
);
assert_eq!(grid.dimensions(), (10, 10));
}
#[test]
fn test_resize_zero_height_error() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.resize(10, 0);
assert!(
matches!(
result,
Err(DotmaxError::InvalidDimensions {
width: 10,
height: 0
})
),
"Resize to height=0 should return InvalidDimensions error"
);
assert_eq!(grid.dimensions(), (10, 10));
}
#[test]
fn test_resize_exceeds_max_width_error() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.resize(20000, 10);
assert!(
matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
"Resize to width=20000 should return InvalidDimensions error"
);
assert_eq!(grid.dimensions(), (10, 10));
}
#[test]
fn test_resize_exceeds_max_height_error() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = grid.resize(10, 20000);
assert!(
matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
"Resize to height=20000 should return InvalidDimensions error"
);
assert_eq!(grid.dimensions(), (10, 10));
}
#[test]
fn test_resize_maintains_invariants() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.resize(20, 15).unwrap();
assert_eq!(
grid.patterns.len(),
300,
"Patterns buffer should have 300 cells for 20×15 grid"
);
assert_eq!(
grid.colors.len(),
300,
"Colors buffer should have 300 cells for 20×15 grid"
);
assert_eq!(grid.width(), 20);
assert_eq!(grid.height(), 15);
}
#[test]
fn test_resize_from_tiny_to_large() {
let mut grid = BrailleGrid::new(1, 1).unwrap();
grid.set_dot(0, 0).unwrap();
grid.resize(50, 30).unwrap();
assert_eq!(grid.dimensions(), (50, 30));
assert!(
grid.get_dot(0, 0, 0).unwrap(),
"Single dot should be preserved at (0,0)"
);
assert!(
!grid.get_dot(10, 10, 0).unwrap(),
"New cells should be empty"
);
}
#[test]
fn test_resize_shrink_to_tiny() {
let mut grid = BrailleGrid::new(50, 30).unwrap();
grid.set_dot(0, 0).unwrap();
grid.set_dot(10, 10).unwrap();
grid.resize(1, 1).unwrap();
assert_eq!(grid.dimensions(), (1, 1));
assert!(
grid.get_dot(0, 0, 0).unwrap(),
"Top-left dot should be preserved"
);
assert!(grid.get_dot(10, 10, 0).is_err());
}
#[test]
fn test_enable_color_support_allocates_buffer() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
assert_eq!(grid.colors.len(), 100); }
#[test]
fn test_set_cell_color_assigns_color() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let red = Color::rgb(255, 0, 0);
grid.set_cell_color(5, 5, red).unwrap();
assert_eq!(grid.get_color(5, 5), Some(red));
}
#[test]
fn test_set_cell_color_valid_coordinates() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let blue = Color::rgb(0, 0, 255);
grid.set_cell_color(0, 0, blue).unwrap();
assert_eq!(grid.get_color(0, 0), Some(blue));
let green = Color::rgb(0, 255, 0);
grid.set_cell_color(9, 9, green).unwrap();
assert_eq!(grid.get_color(9, 9), Some(green));
let yellow = Color::rgb(255, 255, 0);
grid.set_cell_color(5, 5, yellow).unwrap();
assert_eq!(grid.get_color(5, 5), Some(yellow));
}
#[test]
fn test_set_cell_color_out_of_bounds_error() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let result = grid.set_cell_color(100, 100, Color::black());
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_set_cell_color_out_of_bounds_x() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let result = grid.set_cell_color(10, 5, Color::white());
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_set_cell_color_out_of_bounds_y() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let result = grid.set_cell_color(5, 10, Color::white());
assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
}
#[test]
fn test_get_color_none_when_not_set() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
assert_eq!(grid.get_color(5, 5), None);
}
#[test]
fn test_get_color_none_when_out_of_bounds() {
let grid = BrailleGrid::new(10, 10).unwrap();
assert_eq!(grid.get_color(100, 100), None);
}
#[test]
fn test_get_color_returns_color_after_set() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let magenta = Color::rgb(255, 0, 255);
grid.set_cell_color(7, 7, magenta).unwrap();
assert_eq!(grid.get_color(7, 7), Some(magenta));
}
#[test]
fn test_clear_colors_resets_all() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
grid.set_cell_color(7, 7, Color::rgb(0, 255, 0)).unwrap();
grid.set_cell_color(2, 2, Color::rgb(0, 0, 255)).unwrap();
assert!(grid.get_color(5, 5).is_some());
assert!(grid.get_color(7, 7).is_some());
assert!(grid.get_color(2, 2).is_some());
grid.clear_colors();
assert_eq!(grid.get_color(5, 5), None);
assert_eq!(grid.get_color(7, 7), None);
assert_eq!(grid.get_color(2, 2), None);
}
#[test]
fn test_clear_colors_preserves_dots() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
grid.set_dot(10, 20).unwrap();
grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
grid.clear_colors();
assert!(!grid.is_empty(5, 5));
assert_eq!(grid.get_color(5, 5), None);
}
#[test]
fn test_color_partial_eq() {
let red1 = Color::rgb(255, 0, 0);
let red2 = Color::rgb(255, 0, 0);
let blue = Color::rgb(0, 0, 255);
assert_eq!(red1, red2);
assert_ne!(red1, blue);
}
#[test]
fn test_colors_persist_after_resize_grow() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
let purple = Color::rgb(128, 0, 128);
grid.set_cell_color(5, 5, purple).unwrap();
grid.resize(20, 20).unwrap();
assert_eq!(grid.get_color(5, 5), Some(purple));
assert_eq!(grid.get_color(15, 15), None);
}
#[test]
fn test_colors_truncated_after_resize_shrink() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
grid.enable_color_support();
let orange = Color::rgb(255, 165, 0);
grid.set_cell_color(5, 5, orange).unwrap();
grid.set_cell_color(15, 15, Color::rgb(0, 255, 255))
.unwrap();
grid.resize(10, 10).unwrap();
assert_eq!(grid.get_color(5, 5), Some(orange));
assert_eq!(grid.get_color(15, 15), None);
}
#[test]
fn test_enable_color_support_idempotent() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
grid.enable_color_support();
grid.enable_color_support();
assert_eq!(grid.colors.len(), 100);
}
#[test]
fn test_set_cell_color_with_predefined_colors() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
grid.set_cell_color(0, 0, Color::black()).unwrap();
assert_eq!(grid.get_color(0, 0), Some(Color::rgb(0, 0, 0)));
grid.set_cell_color(1, 1, Color::white()).unwrap();
assert_eq!(grid.get_color(1, 1), Some(Color::rgb(255, 255, 255)));
grid.set_cell_color(2, 2, Color::rgb(128, 64, 32)).unwrap();
assert_eq!(grid.get_color(2, 2), Some(Color::rgb(128, 64, 32)));
}
#[test]
fn test_clear_also_clears_colors() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.enable_color_support();
grid.set_dot(10, 20).unwrap();
grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
grid.clear();
assert!(grid.is_empty(5, 5));
assert_eq!(grid.get_color(5, 5), None);
}
#[test]
fn test_resize_preserves_complex_pattern() {
let mut grid = BrailleGrid::new(15, 15).unwrap();
for i in 0..10 {
grid.set_dot(i * 2, i * 4).unwrap();
}
let mut original_patterns = Vec::new();
for i in 0..10 {
let cell_x = (i * 2) / 2;
let cell_y = (i * 4) / 4;
let cell_index = cell_y * 15 + cell_x;
original_patterns.push((cell_x, cell_y, grid.patterns[cell_index]));
}
grid.resize(20, 20).unwrap();
for (cell_x, cell_y, pattern) in &original_patterns {
let new_index = cell_y * 20 + cell_x;
assert_eq!(
grid.patterns[new_index], *pattern,
"Cell ({cell_x}, {cell_y}) pattern should be preserved after grow"
);
}
grid.resize(8, 8).unwrap();
for (cell_x, cell_y, pattern) in &original_patterns {
if *cell_x < 8 && *cell_y < 8 {
let new_index = cell_y * 8 + cell_x;
assert_eq!(
grid.patterns[new_index], *pattern,
"Cell ({cell_x}, {cell_y}) pattern should be preserved after shrink"
);
}
}
}
#[test]
fn test_instrumentation_compiles() {
let grid = BrailleGrid::new(10, 10);
assert!(grid.is_ok());
}
#[test]
fn test_logging_with_subscriber_initialized() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.with_test_writer()
.try_init();
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.clear();
grid.resize(20, 20).unwrap();
grid.enable_color_support();
assert_eq!(grid.dimensions(), (20, 20));
}
#[test]
fn test_logging_silent_by_default() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.clear();
assert_eq!(grid.dimensions(), (10, 10));
}
#[test]
fn test_hot_paths_no_debug_logs() {
let mut grid = BrailleGrid::new(100, 100).unwrap();
for y in 0..200 {
for x in 0..200 {
let _ = grid.set_dot(x, y);
}
}
assert_eq!(grid.dimensions(), (100, 100));
}
#[test]
fn test_error_logging_includes_context() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result1 = BrailleGrid::new(0, 0);
assert!(result1.is_err());
let result2 = grid.set_dot(1000, 1000);
assert!(result2.is_err());
let result3 = grid.set_cell_color(1000, 1000, Color::black());
assert!(result3.is_err());
}
#[test]
fn test_instrumented_functions_correct_types() {
let result1: Result<BrailleGrid, DotmaxError> = BrailleGrid::new(10, 10);
assert!(result1.is_ok());
let mut grid = result1.unwrap();
let result2: () = grid.clear();
assert_eq!(result2, ());
let result3: Result<(), DotmaxError> = grid.resize(20, 20);
assert!(result3.is_ok());
let result4: () = grid.enable_color_support();
assert_eq!(result4, ());
}
#[test]
fn test_logging_in_full_workflow() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.with_test_writer()
.try_init();
let mut grid = BrailleGrid::new(20, 20).unwrap();
grid.enable_color_support();
grid.set_dot(10, 20).unwrap();
grid.set_dot(30, 60).unwrap();
grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
grid.resize(30, 30).unwrap();
grid.clear();
assert_eq!(grid.dimensions(), (30, 30));
assert!(grid.is_empty(0, 0));
}
#[test]
fn test_apply_color_scheme_basic() {
use crate::color::schemes::grayscale;
let mut grid = BrailleGrid::new(4, 3).unwrap();
let intensities: Vec<f32> = (0..12).map(|i| i as f32 / 11.0).collect();
let scheme = grayscale();
let result = grid.apply_color_scheme(&intensities, &scheme);
assert!(result.is_ok());
let c0 = grid.get_color(0, 0).unwrap();
assert_eq!(c0, Color::black());
let c_last = grid.get_color(3, 2).unwrap();
assert_eq!(c_last, Color::white());
}
#[test]
fn test_apply_color_scheme_buffer_mismatch() {
use crate::color::schemes::grayscale;
let mut grid = BrailleGrid::new(4, 3).unwrap();
let too_short: Vec<f32> = vec![0.5; 10]; let too_long: Vec<f32> = vec![0.5; 15]; let scheme = grayscale();
let result = grid.apply_color_scheme(&too_short, &scheme);
assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
let result = grid.apply_color_scheme(&too_long, &scheme);
assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
}
#[test]
fn test_apply_color_scheme_special_floats() {
use crate::color::schemes::grayscale;
let mut grid = BrailleGrid::new(5, 1).unwrap();
let intensities = vec![
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
-0.5,
1.5,
];
let scheme = grayscale();
let result = grid.apply_color_scheme(&intensities, &scheme);
assert!(result.is_ok());
assert_eq!(grid.get_color(0, 0), Some(Color::black()));
assert_eq!(grid.get_color(1, 0), Some(Color::white()));
assert_eq!(grid.get_color(2, 0), Some(Color::black()));
assert_eq!(grid.get_color(3, 0), Some(Color::black()));
assert_eq!(grid.get_color(4, 0), Some(Color::white()));
}
#[test]
fn test_apply_color_scheme_heat_map() {
use crate::color::schemes::heat_map;
let mut grid = BrailleGrid::new(5, 1).unwrap();
let intensities = vec![0.0, 0.25, 0.5, 0.75, 1.0];
let scheme = heat_map();
grid.apply_color_scheme(&intensities, &scheme).unwrap();
assert_eq!(grid.get_color(0, 0), Some(Color::black()));
assert_eq!(grid.get_color(4, 0), Some(Color::white()));
let mid = grid.get_color(2, 0).unwrap();
assert!(mid.r > 0 || mid.g > 0 || mid.b > 0);
}
#[test]
fn test_apply_color_scheme_rainbow() {
use crate::color::schemes::rainbow;
let mut grid = BrailleGrid::new(3, 1).unwrap();
let intensities = vec![0.0, 0.5, 1.0];
let scheme = rainbow();
grid.apply_color_scheme(&intensities, &scheme).unwrap();
let red = grid.get_color(0, 0).unwrap();
assert_eq!(red.r, 255);
assert_eq!(red.g, 0);
assert_eq!(red.b, 0);
let purple = grid.get_color(2, 0).unwrap();
assert!(purple.r > 200);
assert!(purple.b > 200);
}
#[test]
fn test_apply_color_scheme_colors_renderable() {
use crate::color::schemes::heat_map;
let mut grid = BrailleGrid::new(10, 10).unwrap();
grid.set_dot(5, 5).unwrap();
grid.set_dot(10, 10).unwrap();
let intensities: Vec<f32> = (0..100)
.map(|i| (i as f32) / 99.0)
.collect();
let scheme = heat_map();
grid.apply_color_scheme(&intensities, &scheme).unwrap();
for y in 0..10 {
for x in 0..10 {
assert!(grid.get_color(x, y).is_some());
}
}
assert!(!grid.is_empty(2, 1)); assert!(!grid.is_empty(5, 2)); }
#[test]
fn test_apply_color_scheme_single_cell() {
use crate::color::schemes::grayscale;
let mut grid = BrailleGrid::new(1, 1).unwrap();
let intensities = vec![0.5];
let scheme = grayscale();
grid.apply_color_scheme(&intensities, &scheme).unwrap();
let color = grid.get_color(0, 0).unwrap();
assert!(color.r >= 127 && color.r <= 128);
}
}