dotmax/grid.rs
1//! Core [`BrailleGrid`] data structure for terminal braille rendering.
2//!
3//! This module provides the central [`BrailleGrid`] type and [`Color`] struct
4//! for high-resolution terminal graphics using Unicode braille characters.
5//!
6//! # Overview
7//!
8//! Each terminal cell displays a braille character (U+2800-U+28FF) representing
9//! a 2×4 dot matrix. This gives 4× the resolution of ASCII art.
10//!
11//! # Examples
12//!
13//! ```
14//! use dotmax::BrailleGrid;
15//!
16//! // Create 80×24 grid (= 160×96 dot resolution)
17//! let mut grid = BrailleGrid::new(80, 24).unwrap();
18//!
19//! // Set individual dots
20//! grid.set_dot(0, 0).unwrap(); // Top-left
21//! grid.set_dot(159, 95).unwrap(); // Bottom-right
22//!
23//! // Get the grid dimensions
24//! let (width, height) = grid.dimensions();
25//! assert_eq!(width, 80);
26//! assert_eq!(height, 24);
27//! ```
28//!
29//! # Extraction Note
30//!
31//! This module was extracted from the [crabmusic](https://github.com/newjordan/crabmusic) project.
32//! See ADR 0005 (Copy-Refactor-Test strategy) for details.
33
34// Import error types from error module
35use crate::error::DotmaxError;
36
37// Tracing for structured logging (Story 2.7)
38use tracing::{debug, error, info, instrument};
39
40/// Maximum grid dimensions to prevent OOM attacks (NFR-S2)
41const MAX_GRID_WIDTH: usize = 10_000;
42const MAX_GRID_HEIGHT: usize = 10_000;
43
44// ============================================================================
45// Color struct - Extracted from crabmusic/src/visualization/mod.rs
46// ============================================================================
47
48/// RGB color representation for braille cells
49///
50/// Extracted from crabmusic. Story 2.6 will implement full color rendering.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct Color {
53 /// Red component (0-255)
54 pub r: u8,
55 /// Green component (0-255)
56 pub g: u8,
57 /// Blue component (0-255)
58 pub b: u8,
59}
60
61impl Color {
62 /// Create a new RGB color
63 ///
64 /// Extracted from `crabmusic::Color::new()`
65 #[must_use]
66 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
67 Self { r, g, b }
68 }
69
70 /// Create black color (0, 0, 0)
71 #[must_use]
72 pub const fn black() -> Self {
73 Self { r: 0, g: 0, b: 0 }
74 }
75
76 /// Create white color (255, 255, 255)
77 #[must_use]
78 pub const fn white() -> Self {
79 Self {
80 r: 255,
81 g: 255,
82 b: 255,
83 }
84 }
85}
86
87// ============================================================================
88// BrailleDot enum - Extracted from crabmusic/src/visualization/braille.rs:16-28
89// ============================================================================
90
91/// Braille dot positions
92///
93/// Extracted from crabmusic. Maps dot positions to bit patterns for Unicode braille.
94///
95/// Dot positions in a Braille character:
96/// 1 4
97/// 2 5
98/// 3 6
99/// 7 8
100#[repr(u8)]
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum BrailleDot {
103 /// Top-left dot (position 1)
104 Dot1 = 0b0000_0001,
105 /// Middle-left dot (position 2)
106 Dot2 = 0b0000_0010,
107 /// Lower-left dot (position 3)
108 Dot3 = 0b0000_0100,
109 /// Top-right dot (position 4)
110 Dot4 = 0b0000_1000,
111 /// Middle-right dot (position 5)
112 Dot5 = 0b0001_0000,
113 /// Lower-right dot (position 6)
114 Dot6 = 0b0010_0000,
115 /// Bottom-left dot (position 7)
116 Dot7 = 0b0100_0000,
117 /// Bottom-right dot (position 8)
118 Dot8 = 0b1000_0000,
119}
120
121// ============================================================================
122// dots_to_char - Extracted from crabmusic/src/visualization/braille.rs:52-56
123// ============================================================================
124
125/// Convert dot pattern to Braille Unicode character
126///
127/// Extracted from crabmusic. Story 2.2 will integrate this into the rendering pipeline.
128///
129/// # Arguments
130/// * `dots` - Bit pattern where each bit represents a dot (1 = filled)
131///
132/// # Returns
133/// Unicode Braille character
134///
135/// # Examples
136///
137/// ```
138/// use dotmax::grid::dots_to_char;
139///
140/// // Empty pattern
141/// assert_eq!(dots_to_char(0b00000000), '⠀');
142///
143/// // All dots filled
144/// assert_eq!(dots_to_char(0b11111111), '⣿');
145/// ```
146#[inline]
147#[must_use]
148pub fn dots_to_char(dots: u8) -> char {
149 // Braille patterns start at U+2800
150 // SAFETY: crabmusic uses unwrap_or here; we keep the same logic
151 // since 0x2800 + (0..=255) is always valid Unicode
152 char::from_u32(0x2800 + u32::from(dots)).unwrap_or('⠀')
153}
154
155// ============================================================================
156// BrailleGrid - Extracted from crabmusic/src/visualization/braille.rs:73-369
157// ============================================================================
158
159/// High-resolution grid using Braille characters
160///
161/// **Extracted from crabmusic** - Battle-tested rendering engine.
162///
163/// Each terminal cell contains a 2×4 dot pattern (8 dots total), giving us
164/// high-resolution graphics in any terminal that supports Unicode braille.
165///
166/// ## Architecture (Preserved from crabmusic)
167///
168/// - **`patterns: Vec<u8>`** - Flat array, each u8 is a bitfield (8 bits = 8 dots)
169/// - **Dot coordinates**: (`dot_x`, `dot_y`) in pixel space (width*2 × height*4)
170/// - **Cell coordinates**: (`cell_x`, `cell_y`) in terminal space (width × height)
171///
172/// ## Dot Indexing (Unicode Braille Standard)
173///
174/// ```text
175/// Braille cell (8 dots):
176/// 1 4 (Dot1=0x01, Dot4=0x08)
177/// 2 5 (Dot2=0x02, Dot5=0x10)
178/// 3 6 (Dot3=0x04, Dot6=0x20)
179/// 7 8 (Dot7=0x40, Dot8=0x80)
180/// ```
181///
182/// # Example
183///
184/// ```
185/// use dotmax::BrailleGrid;
186///
187/// let mut grid = BrailleGrid::new(40, 20).unwrap();
188/// // Grid is 40×20 cells = 80×80 dot resolution
189/// grid.set_dot(0, 0); // Top-left dot
190/// grid.set_dot(1, 0); // Top-right dot of first cell
191/// ```
192#[derive(Debug, Clone)]
193pub struct BrailleGrid {
194 /// Width in terminal cells
195 width: usize,
196 /// Height in terminal cells
197 height: usize,
198 /// Dot patterns for each cell (binary on/off)
199 ///
200 /// **Preserved from crabmusic**: `Vec<u8>` bitfield representation
201 /// Each `u8` represents one terminal cell with 8 dots
202 patterns: Vec<u8>,
203 /// Optional colors for each cell
204 ///
205 /// **Preserved from crabmusic**: `Vec<Option<Color>>`
206 /// Story 2.6 will implement color rendering
207 colors: Vec<Option<Color>>,
208 /// Optional text characters for density rendering (Story 4.4)
209 ///
210 /// When set, these characters override braille dot patterns for rendering.
211 /// This enables character-density based rendering (ASCII art style) as an
212 /// alternative to binary braille dot rendering.
213 ///
214 /// `None` = use braille dots (default), `Some(char)` = render this character
215 characters: Vec<Option<char>>,
216}
217
218impl BrailleGrid {
219 /// Create a new Braille grid
220 ///
221 /// **Extracted from `crabmusic::BrailleGrid::new()`** with added validation.
222 ///
223 /// # Arguments
224 /// * `width` - Width in terminal cells (must be > 0 and <= `MAX_GRID_WIDTH`)
225 /// * `height` - Height in terminal cells (must be > 0 and <= `MAX_GRID_HEIGHT`)
226 ///
227 /// # Returns
228 /// * `Ok(BrailleGrid)` if dimensions are valid
229 /// * `Err(DotmaxError::InvalidDimensions)` if width/height is 0 or exceeds max
230 ///
231 /// # Errors
232 /// Returns `InvalidDimensions` if width or height is 0 or exceeds max allowed dimensions.
233 ///
234 /// # Crabmusic Change
235 /// Original crabmusic code never validated dimensions.
236 /// Dotmax adds validation for security (NFR-S2).
237 #[instrument]
238 pub fn new(width: usize, height: usize) -> Result<Self, DotmaxError> {
239 // Validate dimensions (NEW - not in crabmusic)
240 if width == 0 || height == 0 {
241 error!(
242 width = width,
243 height = height,
244 "Invalid grid dimensions: width or height is zero"
245 );
246 return Err(DotmaxError::InvalidDimensions { width, height });
247 }
248
249 if width > MAX_GRID_WIDTH || height > MAX_GRID_HEIGHT {
250 error!(
251 width = width,
252 height = height,
253 max_width = MAX_GRID_WIDTH,
254 max_height = MAX_GRID_HEIGHT,
255 "Invalid grid dimensions: exceeds maximum allowed size"
256 );
257 return Err(DotmaxError::InvalidDimensions { width, height });
258 }
259
260 // Allocate grid (PRESERVED from crabmusic)
261 let size = width * height;
262 info!(
263 width = width,
264 height = height,
265 total_cells = size,
266 "Creating BrailleGrid"
267 );
268 Ok(Self {
269 width,
270 height,
271 patterns: vec![0; size],
272 colors: vec![None; size],
273 characters: vec![None; size], // Story 4.4: character buffer for density rendering
274 })
275 }
276
277 /// Get width in terminal cells
278 ///
279 /// **Extracted from crabmusic** (lines 104-106)
280 #[must_use]
281 pub const fn width(&self) -> usize {
282 self.width
283 }
284
285 /// Get height in terminal cells
286 ///
287 /// **Extracted from crabmusic** (lines 108-111)
288 #[must_use]
289 pub const fn height(&self) -> usize {
290 self.height
291 }
292
293 /// Get width in dots (2× terminal width)
294 ///
295 /// **Extracted from crabmusic** (lines 113-116)
296 #[must_use]
297 pub const fn dot_width(&self) -> usize {
298 self.width * 2
299 }
300
301 /// Get height in dots (4× terminal height)
302 ///
303 /// **Extracted from crabmusic** (lines 118-121)
304 #[must_use]
305 pub const fn dot_height(&self) -> usize {
306 self.height * 4
307 }
308
309 /// Get the dimensions of the grid (dotmax addition for AC #7)
310 ///
311 /// **NEW** - Not in crabmusic. Added to satisfy AC #7 requirement.
312 ///
313 /// # Returns
314 /// A tuple of (width, height) in terminal cells
315 #[must_use]
316 pub const fn dimensions(&self) -> (usize, usize) {
317 (self.width, self.height)
318 }
319
320 /// Clear all dots
321 ///
322 /// **Extracted from crabmusic** (lines 124-127) with minor adaptation
323 #[instrument(skip(self))]
324 pub fn clear(&mut self) {
325 debug!(
326 width = self.width,
327 height = self.height,
328 "Clearing all dots in grid"
329 );
330 self.patterns.fill(0);
331 self.colors.fill(None);
332 }
333
334 /// Set a single dot at the specified position
335 ///
336 /// **Extracted from crabmusic** (lines 144-172) with added error handling.
337 ///
338 /// **CRITICAL**: This uses PIXEL coordinates (`dot_x`, `dot_y`), not cell coordinates.
339 /// The grid is width*2 × height*4 dots.
340 ///
341 /// # Arguments
342 /// * `dot_x` - X position in dots (0 to width*2-1)
343 /// * `dot_y` - Y position in dots (0 to height*4-1)
344 ///
345 /// # Crabmusic Change
346 /// Original crabmusic silently ignored out-of-bounds coordinates.
347 /// Dotmax returns an error for explicit bounds checking (zero panics policy).
348 ///
349 /// # Errors
350 /// Returns `OutOfBounds` if dot coordinates exceed grid dimensions.
351 pub fn set_dot(&mut self, dot_x: usize, dot_y: usize) -> Result<(), DotmaxError> {
352 // Bounds check (MODIFIED from crabmusic - return error instead of silent ignore)
353 if dot_x >= self.dot_width() || dot_y >= self.dot_height() {
354 error!(
355 dot_x = dot_x,
356 dot_y = dot_y,
357 dot_width = self.dot_width(),
358 dot_height = self.dot_height(),
359 "Out of bounds dot access: ({}, {}) in grid of size ({}, {})",
360 dot_x,
361 dot_y,
362 self.dot_width(),
363 self.dot_height()
364 );
365 return Err(DotmaxError::OutOfBounds {
366 x: dot_x,
367 y: dot_y,
368 width: self.dot_width(),
369 height: self.dot_height(),
370 });
371 }
372
373 // Convert dot coordinates to cell coordinates (PRESERVED from crabmusic)
374 let cell_x = dot_x / 2;
375 let cell_y = dot_y / 4;
376 let cell_index = cell_y * self.width + cell_x;
377
378 // Determine which dot within the cell (0-7) (PRESERVED from crabmusic)
379 let local_x = dot_x % 2;
380 let local_y = dot_y % 4;
381
382 // Map to Braille dot position (PRESERVED from crabmusic, lines 159-169)
383 let dot_bit = match (local_x, local_y) {
384 (0, 0) => BrailleDot::Dot1 as u8,
385 (0, 1) => BrailleDot::Dot2 as u8,
386 (0, 2) => BrailleDot::Dot3 as u8,
387 (0, 3) => BrailleDot::Dot7 as u8,
388 (1, 0) => BrailleDot::Dot4 as u8,
389 (1, 1) => BrailleDot::Dot5 as u8,
390 (1, 2) => BrailleDot::Dot6 as u8,
391 (1, 3) => BrailleDot::Dot8 as u8,
392 _ => unreachable!(),
393 };
394
395 // Set the dot (PRESERVED from crabmusic, line 171)
396 self.patterns[cell_index] |= dot_bit;
397 Ok(())
398 }
399
400 /// Get an individual dot value
401 ///
402 /// **NEW** - Not in crabmusic. Added to match AC #4 requirement.
403 ///
404 /// # Arguments
405 /// * `dot_x` - X position in dots (0 to width*2-1)
406 /// * `dot_y` - Y position in dots (0 to height*4-1)
407 /// * `dot_index` - Dot position 0-7 in the cell
408 ///
409 /// # Returns
410 /// * `Ok(bool)` - The dot value (true = enabled, false = disabled)
411 /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
412 /// * `Err(DotmaxError::InvalidDotIndex)` if `dot_index` > 7
413 ///
414 /// # Errors
415 /// Returns `OutOfBounds` if dot coordinates exceed grid dimensions, or `InvalidDotIndex` if dot index > 7.
416 pub fn get_dot(&self, x: usize, y: usize, dot_index: u8) -> Result<bool, DotmaxError> {
417 // Validate cell bounds
418 if x >= self.width || y >= self.height {
419 return Err(DotmaxError::OutOfBounds {
420 x,
421 y,
422 width: self.width,
423 height: self.height,
424 });
425 }
426
427 // Validate dot index
428 if dot_index > 7 {
429 return Err(DotmaxError::InvalidDotIndex { index: dot_index });
430 }
431
432 // Calculate cell index
433 let cell_index = y * self.width + x;
434 let pattern = self.patterns[cell_index];
435
436 // Check if dot is set using bit mask
437 let dot_bit = 1u8 << dot_index;
438 Ok((pattern & dot_bit) != 0)
439 }
440
441 /// Clear a rectangular region of the grid
442 ///
443 /// **NEW** - Not in crabmusic. Added to satisfy AC #6 requirement.
444 ///
445 /// # Arguments
446 /// * `x` - Starting column in cells (0-indexed)
447 /// * `y` - Starting row in cells (0-indexed)
448 /// * `width` - Width of region to clear in cells
449 /// * `height` - Height of region to clear in cells
450 ///
451 /// # Returns
452 /// * `Ok(())` if region was cleared successfully
453 /// * `Err(DotmaxError::OutOfBounds)` if region extends beyond grid bounds
454 ///
455 /// # Errors
456 /// Returns `OutOfBounds` if the specified region extends beyond grid dimensions.
457 pub fn clear_region(
458 &mut self,
459 x: usize,
460 y: usize,
461 width: usize,
462 height: usize,
463 ) -> Result<(), DotmaxError> {
464 // Validate bounds - check if region fits within grid
465 let end_x = x.saturating_add(width);
466 let end_y = y.saturating_add(height);
467
468 if end_x > self.width || end_y > self.height {
469 return Err(DotmaxError::OutOfBounds {
470 x: end_x.saturating_sub(1),
471 y: end_y.saturating_sub(1),
472 width: self.width,
473 height: self.height,
474 });
475 }
476
477 // Clear the specified region
478 for row_idx in y..end_y {
479 for col_idx in x..end_x {
480 let cell_index = row_idx * self.width + col_idx;
481 self.patterns[cell_index] = 0;
482 self.colors[cell_index] = None;
483 }
484 }
485
486 Ok(())
487 }
488
489 /// Get the Braille character at a cell position
490 ///
491 /// **Extracted from crabmusic** (lines 338-347)
492 ///
493 /// # Arguments
494 /// * `cell_x` - X position in cells
495 /// * `cell_y` - Y position in cells
496 ///
497 /// # Returns
498 /// Character at the cell position:
499 /// - If a text character is set (Story 4.4 density rendering), returns that character
500 /// - Otherwise, returns braille character representing the dot pattern
501 #[must_use]
502 pub fn get_char(&self, cell_x: usize, cell_y: usize) -> char {
503 if cell_x >= self.width || cell_y >= self.height {
504 return '⠀';
505 }
506
507 let index = cell_y * self.width + cell_x;
508
509 // Story 4.4: Prioritize text characters for density rendering
510 if let Some(ch) = self.characters[index] {
511 return ch;
512 }
513
514 // Default: Convert dot pattern to braille character
515 dots_to_char(self.patterns[index])
516 }
517
518 /// Get the color at a cell position
519 ///
520 /// **Extracted from crabmusic** (lines 350-357)
521 #[must_use]
522 pub fn get_color(&self, cell_x: usize, cell_y: usize) -> Option<Color> {
523 if cell_x >= self.width || cell_y >= self.height {
524 return None;
525 }
526
527 let index = cell_y * self.width + cell_x;
528 self.colors[index]
529 }
530
531 /// Check if a cell has any dots set
532 ///
533 /// **Extracted from crabmusic** (lines 360-368)
534 #[must_use]
535 pub fn is_empty(&self, cell_x: usize, cell_y: usize) -> bool {
536 if cell_x >= self.width || cell_y >= self.height {
537 return true;
538 }
539
540 let index = cell_y * self.width + cell_x;
541 self.patterns[index] == 0
542 }
543
544 // ========================================================================
545 // Story 6.4: Raw Pattern Access for Animation Serialization
546 // ========================================================================
547
548 /// Get raw access to the pattern buffer for serialization.
549 ///
550 /// **Story 6.4** - Enables efficient animation frame serialization.
551 ///
552 /// Returns a slice of the internal pattern buffer, where each byte
553 /// represents one braille cell's dot pattern (8 bits = 8 dots).
554 ///
555 /// # Returns
556 /// A byte slice of length `width * height` containing dot patterns.
557 ///
558 /// # Examples
559 /// ```
560 /// use dotmax::BrailleGrid;
561 ///
562 /// let mut grid = BrailleGrid::new(10, 5).unwrap();
563 /// grid.set_dot(0, 0).unwrap(); // Set top-left dot
564 ///
565 /// let patterns = grid.get_raw_patterns();
566 /// assert_eq!(patterns.len(), 50); // 10 * 5 cells
567 /// assert_eq!(patterns[0], 0b0000_0001); // First cell has dot 1 set
568 /// ```
569 #[must_use]
570 pub fn get_raw_patterns(&self) -> &[u8] {
571 &self.patterns
572 }
573
574 /// Set raw pattern buffer from serialized data.
575 ///
576 /// **Story 6.4** - Enables efficient animation frame deserialization.
577 ///
578 /// Copies data into the internal pattern buffer. The data slice length
579 /// must match `width * height`. If the length doesn't match, excess data
580 /// is truncated or missing data is left unchanged.
581 ///
582 /// # Arguments
583 /// * `data` - Raw pattern bytes to copy into the grid
584 ///
585 /// # Examples
586 /// ```
587 /// use dotmax::BrailleGrid;
588 ///
589 /// let mut grid = BrailleGrid::new(10, 5).unwrap();
590 /// let mut patterns = vec![0u8; 50];
591 /// patterns[0] = 0b0000_0001; // Set dot 1 in first cell
592 ///
593 /// grid.set_raw_patterns(&patterns);
594 /// assert!(grid.get_char(0, 0) == '⠁'); // First cell shows dot 1
595 /// ```
596 pub fn set_raw_patterns(&mut self, data: &[u8]) {
597 let copy_len = data.len().min(self.patterns.len());
598 self.patterns[..copy_len].copy_from_slice(&data[..copy_len]);
599 }
600
601 // ========================================================================
602 // Story 2.2: Unicode Braille Character Conversion
603 // ========================================================================
604
605 /// Convert entire grid to 2D array of Unicode braille characters
606 ///
607 /// **Story 2.2** - Batch conversion for rendering pipeline.
608 ///
609 /// This method converts the entire grid from dot patterns to Unicode braille
610 /// characters, producing a 2D array that matches the grid dimensions.
611 ///
612 /// Uses the proven `dots_to_char()` function extracted from crabmusic
613 /// (lines 53-56) which applies the Unicode Braille standard formula:
614 /// `U+2800 + bitfield`
615 ///
616 /// # Returns
617 /// A 2D vector of Unicode braille characters, where `result[y][x]` corresponds
618 /// to cell `(x, y)` in the grid.
619 ///
620 /// # Examples
621 ///
622 /// ```
623 /// use dotmax::BrailleGrid;
624 ///
625 /// let mut grid = BrailleGrid::new(5, 5).unwrap();
626 /// grid.set_dot(0, 0).unwrap(); // Top-left dot of cell (0,0)
627 ///
628 /// let chars = grid.to_unicode_grid();
629 /// assert_eq!(chars.len(), 5); // 5 rows
630 /// assert_eq!(chars[0].len(), 5); // 5 columns
631 /// assert_eq!(chars[0][0], '⠁'); // Cell (0,0) has dot 1 set
632 /// ```
633 ///
634 /// # Performance
635 /// Time complexity: O(width × height) - processes each cell once
636 /// Allocates: `Vec<Vec<char>>` with dimensions matching grid size
637 #[must_use]
638 pub fn to_unicode_grid(&self) -> Vec<Vec<char>> {
639 let mut result = Vec::with_capacity(self.height);
640
641 for y in 0..self.height {
642 let mut row = Vec::with_capacity(self.width);
643 for x in 0..self.width {
644 let index = y * self.width + x;
645 // Use extracted crabmusic conversion function
646 row.push(dots_to_char(self.patterns[index]));
647 }
648 result.push(row);
649 }
650
651 result
652 }
653
654 /// Convert single cell at (x, y) to Unicode braille character
655 ///
656 /// **Story 2.2** - Single-cell conversion with bounds validation.
657 ///
658 /// Returns the Unicode braille character for a specific cell, or an error
659 /// if coordinates are out of bounds.
660 ///
661 /// # Arguments
662 /// * `x` - X position in cells (0 to width-1)
663 /// * `y` - Y position in cells (0 to height-1)
664 ///
665 /// # Returns
666 /// * `Ok(char)` - Unicode braille character (U+2800 to U+28FF)
667 /// * `Err(DotmaxError::OutOfBounds)` - If coordinates exceed grid dimensions
668 ///
669 /// # Examples
670 ///
671 /// ```
672 /// use dotmax::BrailleGrid;
673 ///
674 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
675 /// grid.set_dot(2, 4).unwrap(); // Set a dot in cell (1,1)
676 ///
677 /// let ch = grid.cell_to_braille_char(1, 1).unwrap();
678 /// assert!(ch >= '\u{2800}' && ch <= '\u{28FF}'); // Valid braille range
679 /// ```
680 ///
681 /// # Errors
682 /// Returns `OutOfBounds` if x >= width or y >= height.
683 pub fn cell_to_braille_char(&self, x: usize, y: usize) -> Result<char, DotmaxError> {
684 // Validate bounds
685 if x >= self.width || y >= self.height {
686 return Err(DotmaxError::OutOfBounds {
687 x,
688 y,
689 width: self.width,
690 height: self.height,
691 });
692 }
693
694 // Convert cell pattern to Unicode
695 let index = y * self.width + x;
696 Ok(dots_to_char(self.patterns[index]))
697 }
698
699 /// Resize the grid to new dimensions
700 ///
701 /// **NEW for Story 2.5** - Not in crabmusic. Enables terminal resize handling.
702 ///
703 /// # Arguments
704 /// * `new_width` - New width in braille cells
705 /// * `new_height` - New height in braille cells
706 ///
707 /// # Behavior
708 /// - **Grow**: New cells initialized to empty (pattern=0, color=None)
709 /// - **Shrink**: Existing dots outside new bounds are truncated
710 /// - **Preserve**: Dots within overlap region are preserved
711 /// - **Colors**: Color buffer resizes in sync with patterns
712 ///
713 /// # Errors
714 /// Returns `DotmaxError::InvalidDimensions` if:
715 /// - `new_width` or `new_height` is 0
716 /// - `new_width` or `new_height` exceeds `MAX_GRID_WIDTH`/`MAX_GRID_HEIGHT` (10,000)
717 ///
718 /// # Examples
719 /// ```
720 /// use dotmax::BrailleGrid;
721 ///
722 /// let mut grid = BrailleGrid::new(10, 10)?;
723 /// grid.set_dot(0, 0)?; // Set top-left dot
724 ///
725 /// // Resize to larger dimensions
726 /// grid.resize(20, 20)?;
727 /// assert_eq!(grid.dimensions(), (20, 20));
728 ///
729 /// // Resize to smaller dimensions
730 /// grid.resize(5, 5)?;
731 /// assert_eq!(grid.dimensions(), (5, 5));
732 /// # Ok::<(), dotmax::DotmaxError>(())
733 /// ```
734 #[instrument(skip(self))]
735 pub fn resize(&mut self, new_width: usize, new_height: usize) -> Result<(), DotmaxError> {
736 debug!(
737 old_width = self.width,
738 old_height = self.height,
739 new_width = new_width,
740 new_height = new_height,
741 "Resizing BrailleGrid"
742 );
743
744 // Validation (same logic as new())
745 if new_width == 0 || new_height == 0 {
746 error!(
747 new_width = new_width,
748 new_height = new_height,
749 "Invalid resize dimensions: width or height is zero"
750 );
751 return Err(DotmaxError::InvalidDimensions {
752 width: new_width,
753 height: new_height,
754 });
755 }
756 if new_width > MAX_GRID_WIDTH || new_height > MAX_GRID_HEIGHT {
757 error!(
758 new_width = new_width,
759 new_height = new_height,
760 max_width = MAX_GRID_WIDTH,
761 max_height = MAX_GRID_HEIGHT,
762 "Invalid resize dimensions: exceeds maximum allowed size"
763 );
764 return Err(DotmaxError::InvalidDimensions {
765 width: new_width,
766 height: new_height,
767 });
768 }
769
770 // Create new storage
771 let new_size = new_width * new_height;
772 let mut new_patterns = vec![0; new_size];
773 let mut new_colors = vec![None; new_size];
774
775 // Copy existing data (preserve overlap region)
776 let copy_width = self.width.min(new_width);
777 let copy_height = self.height.min(new_height);
778
779 for y in 0..copy_height {
780 for x in 0..copy_width {
781 let old_index = y * self.width + x;
782 let new_index = y * new_width + x;
783 new_patterns[new_index] = self.patterns[old_index];
784 new_colors[new_index] = self.colors[old_index];
785 }
786 }
787
788 // Update grid state
789 self.width = new_width;
790 self.height = new_height;
791 self.patterns = new_patterns;
792 self.colors = new_colors;
793
794 Ok(())
795 }
796
797 // ========================================================================
798 // Story 2.6: Color Support for Braille Cells
799 // ========================================================================
800
801 /// Enable color support by allocating color buffer
802 ///
803 /// **Story 2.6** - Allocates per-cell color storage.
804 ///
805 /// Note: In the current implementation, the color buffer is always allocated
806 /// during `BrailleGrid::new()`, so this method is a no-op for compatibility
807 /// with the AC specification. It ensures the color buffer exists.
808 ///
809 /// # Examples
810 /// ```
811 /// use dotmax::BrailleGrid;
812 ///
813 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
814 /// grid.enable_color_support(); // Ensures color support is enabled
815 /// ```
816 #[instrument(skip(self))]
817 pub fn enable_color_support(&mut self) {
818 debug!(
819 width = self.width,
820 height = self.height,
821 "Enabling color support (already enabled in current implementation)"
822 );
823 // Color buffer is already allocated in new(), so this is a no-op
824 // This method exists for API compatibility with AC 2.6.3
825 //
826 // If we change to Option<Vec<Option<Color>>> in future, this would be:
827 // if self.colors.is_none() {
828 // self.colors = Some(vec![None; self.width * self.height]);
829 // }
830 }
831
832 /// Assign RGB color to cell at (x, y)
833 ///
834 /// **Story 2.6** - Per-cell color assignment with bounds validation.
835 ///
836 /// Sets the color for a specific cell. The color will be applied when
837 /// rendering via `TerminalRenderer`.
838 ///
839 /// # Arguments
840 /// * `x` - X position in cells (0 to width-1)
841 /// * `y` - Y position in cells (0 to height-1)
842 /// * `color` - RGB color to assign
843 ///
844 /// # Returns
845 /// * `Ok(())` if color was assigned successfully
846 /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
847 ///
848 /// # Examples
849 /// ```
850 /// use dotmax::{BrailleGrid, Color};
851 ///
852 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
853 /// grid.enable_color_support();
854 ///
855 /// // Set cell (5, 5) to red
856 /// grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
857 ///
858 /// // Verify color was set
859 /// assert_eq!(grid.get_color(5, 5), Some(Color::rgb(255, 0, 0)));
860 /// ```
861 ///
862 /// # Errors
863 /// Returns `OutOfBounds` if x >= width or y >= height.
864 pub fn set_cell_color(&mut self, x: usize, y: usize, color: Color) -> Result<(), DotmaxError> {
865 // Validate bounds
866 if x >= self.width || y >= self.height {
867 error!(
868 x = x,
869 y = y,
870 width = self.width,
871 height = self.height,
872 "Out of bounds color assignment: ({}, {}) in grid of size ({}, {})",
873 x,
874 y,
875 self.width,
876 self.height
877 );
878 return Err(DotmaxError::OutOfBounds {
879 x,
880 y,
881 width: self.width,
882 height: self.height,
883 });
884 }
885
886 // Set color
887 let index = y * self.width + x;
888 self.colors[index] = Some(color);
889 Ok(())
890 }
891
892 /// Reset all colors to None (monochrome)
893 ///
894 /// **Story 2.6** - Clear color buffer without deallocating.
895 ///
896 /// Resets all cell colors to `None` while keeping the color buffer
897 /// allocated. This is useful for switching back to monochrome rendering
898 /// without disabling color support entirely.
899 ///
900 /// # Examples
901 /// ```
902 /// use dotmax::{BrailleGrid, Color};
903 ///
904 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
905 /// grid.enable_color_support();
906 ///
907 /// // Set some colors
908 /// grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
909 /// grid.set_cell_color(7, 7, Color::rgb(0, 255, 0)).unwrap();
910 ///
911 /// // Clear all colors
912 /// grid.clear_colors();
913 ///
914 /// // All colors are now None
915 /// assert_eq!(grid.get_color(5, 5), None);
916 /// assert_eq!(grid.get_color(7, 7), None);
917 /// ```
918 pub fn clear_colors(&mut self) {
919 self.colors.fill(None);
920 }
921
922 /// Set a text character at a cell position (Story 4.4)
923 ///
924 /// **Story 4.4** - Character density-based rendering support.
925 ///
926 /// Sets a text character at the specified cell position. When a character is set,
927 /// it overrides the braille dot pattern for that cell during rendering. This enables
928 /// ASCII-art style density rendering as an alternative to binary braille dots.
929 ///
930 /// # Arguments
931 ///
932 /// * `x` - X position in cells (0-indexed)
933 /// * `y` - Y position in cells (0-indexed)
934 /// * `character` - The character to set at this position
935 ///
936 /// # Returns
937 ///
938 /// * `Ok(())` if character was set successfully
939 /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
940 ///
941 /// # Examples
942 ///
943 /// ```
944 /// use dotmax::BrailleGrid;
945 ///
946 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
947 ///
948 /// // Set character at cell (5, 5)
949 /// grid.set_char(5, 5, '@').unwrap();
950 ///
951 /// // Verify character was set
952 /// assert_eq!(grid.get_char(5, 5), '@');
953 /// ```
954 ///
955 /// # Errors
956 ///
957 /// Returns `OutOfBounds` if x >= width or y >= height.
958 pub fn set_char(&mut self, x: usize, y: usize, character: char) -> Result<(), DotmaxError> {
959 // Validate bounds
960 if x >= self.width || y >= self.height {
961 error!(
962 x = x,
963 y = y,
964 width = self.width,
965 height = self.height,
966 "Out of bounds character assignment: ({}, {}) in grid of size ({}, {})",
967 x,
968 y,
969 self.width,
970 self.height
971 );
972 return Err(DotmaxError::OutOfBounds {
973 x,
974 y,
975 width: self.width,
976 height: self.height,
977 });
978 }
979
980 // Set character
981 let index = y * self.width + x;
982 self.characters[index] = Some(character);
983 Ok(())
984 }
985
986 /// Clear all text characters (Story 4.4)
987 ///
988 /// **Story 4.4** - Reset character buffer for density rendering.
989 ///
990 /// Resets all cell characters to `None`, restoring default braille dot rendering
991 /// mode. This is useful for switching back from density rendering to braille dots
992 /// without creating a new grid.
993 ///
994 /// # Examples
995 ///
996 /// ```
997 /// use dotmax::BrailleGrid;
998 ///
999 /// let mut grid = BrailleGrid::new(10, 10).unwrap();
1000 ///
1001 /// // Set some characters
1002 /// grid.set_char(5, 5, '@').unwrap();
1003 /// grid.set_char(7, 7, '#').unwrap();
1004 ///
1005 /// // Clear all characters
1006 /// grid.clear_characters();
1007 ///
1008 /// // All characters are now None (braille mode restored)
1009 /// assert_eq!(grid.get_char(5, 5), '⠀'); // Empty braille pattern
1010 /// ```
1011 pub fn clear_characters(&mut self) {
1012 self.characters.fill(None);
1013 }
1014
1015 // ========================================================================
1016 // Story 5.5: Apply Color Scheme to Intensity Buffer
1017 // ========================================================================
1018
1019 /// Apply a color scheme to a flattened intensity buffer.
1020 ///
1021 /// **Story 5.5** - Convenience method for colorizing intensity data.
1022 ///
1023 /// Maps a 1D intensity buffer to colors using the provided color scheme,
1024 /// populating the grid's color buffer. The intensity buffer must be in
1025 /// row-major order and match the grid dimensions (`width × height`).
1026 ///
1027 /// This method directly modifies the grid's internal color buffer for
1028 /// maximum performance, avoiding intermediate allocations.
1029 ///
1030 /// # Arguments
1031 ///
1032 /// * `intensities` - Flattened 1D intensity buffer (row-major order)
1033 /// * `scheme` - Color scheme for intensity-to-color mapping
1034 ///
1035 /// # Returns
1036 ///
1037 /// * `Ok(())` if colors were applied successfully
1038 /// * `Err(DotmaxError::BufferSizeMismatch)` if buffer length doesn't match grid size
1039 ///
1040 /// # Intensity Handling
1041 ///
1042 /// - Values in range 0.0-1.0 are mapped normally
1043 /// - Values outside range are clamped (consistent with `ColorScheme::sample`)
1044 /// - NaN values are treated as 0.0
1045 /// - Infinity values are clamped to 0.0 or 1.0
1046 ///
1047 /// # Examples
1048 ///
1049 /// ```
1050 /// use dotmax::{BrailleGrid, ColorScheme};
1051 ///
1052 /// let mut grid = BrailleGrid::new(4, 3).unwrap();
1053 ///
1054 /// // Create intensity buffer (4×3 = 12 cells)
1055 /// let intensities: Vec<f32> = (0..12)
1056 /// .map(|i| i as f32 / 11.0)
1057 /// .collect();
1058 ///
1059 /// // Apply heat map scheme
1060 /// let scheme = ColorScheme::heat_map();
1061 /// grid.apply_color_scheme(&intensities, &scheme).unwrap();
1062 ///
1063 /// // First cell is black (intensity 0.0)
1064 /// let c0 = grid.get_color(0, 0).unwrap();
1065 /// assert_eq!(c0.r, 0);
1066 ///
1067 /// // Last cell is white (intensity 1.0)
1068 /// let c11 = grid.get_color(3, 2).unwrap();
1069 /// assert_eq!(c11.r, 255);
1070 /// ```
1071 ///
1072 /// # Integration with Epic 3 Image Pipeline
1073 ///
1074 /// ```no_run
1075 /// use dotmax::{BrailleGrid, ColorScheme};
1076 /// # // Note: This requires the image feature
1077 /// # // Load image -> Grayscale -> Intensity buffer -> Color scheme
1078 /// ```
1079 ///
1080 /// # Performance
1081 ///
1082 /// Target: <10ms for 80×24 grid (1,920 cells)
1083 ///
1084 /// # Errors
1085 ///
1086 /// Returns [`DotmaxError::BufferSizeMismatch`] if `intensities.len() != width × height`.
1087 pub fn apply_color_scheme(
1088 &mut self,
1089 intensities: &[f32],
1090 scheme: &crate::color::schemes::ColorScheme,
1091 ) -> Result<(), DotmaxError> {
1092 let expected_len = self.width * self.height;
1093
1094 // Validate buffer size
1095 if intensities.len() != expected_len {
1096 return Err(DotmaxError::BufferSizeMismatch {
1097 expected: expected_len,
1098 actual: intensities.len(),
1099 });
1100 }
1101
1102 // Apply colors directly to internal buffer
1103 for (index, &intensity) in intensities.iter().enumerate() {
1104 // Handle special float values and clamp
1105 let normalized = if intensity.is_nan() {
1106 0.0
1107 } else if intensity.is_infinite() {
1108 if intensity.is_sign_positive() {
1109 1.0
1110 } else {
1111 0.0
1112 }
1113 } else {
1114 intensity.clamp(0.0, 1.0)
1115 };
1116
1117 self.colors[index] = Some(scheme.sample(normalized));
1118 }
1119
1120 Ok(())
1121 }
1122}
1123
1124// ============================================================================
1125// STRIPPED from crabmusic - Not in Story 2.1 scope:
1126// ============================================================================
1127// - set_dot_with_color() → Story 2.6 (Color Support)
1128// - draw_line() / draw_line_with_color() → Epic 4 (Drawing Primitives)
1129// - draw_circle() → Epic 4 (Drawing Primitives)
1130// - Anti-aliasing logic → Out of scope (audio-reactive feature)
1131// - FFT/spectrum integration → Audio dependencies (discarded)
1132// ============================================================================
1133
1134#[cfg(test)]
1135mod tests {
1136 use super::*;
1137
1138 // ========================================================================
1139 // Grid Creation Tests (AC #2) - Adapted from crabmusic tests
1140 // ========================================================================
1141
1142 #[test]
1143 fn test_new_valid_dimensions() {
1144 let grid = BrailleGrid::new(80, 24);
1145 assert!(grid.is_ok());
1146 let grid = grid.unwrap();
1147 assert_eq!(grid.dimensions(), (80, 24));
1148 assert_eq!(grid.width(), 80);
1149 assert_eq!(grid.height(), 24);
1150 }
1151
1152 #[test]
1153 fn test_braille_grid_creation() {
1154 // Ported from crabmusic test_braille_grid_creation (line 389)
1155 let grid = BrailleGrid::new(40, 20).unwrap();
1156 assert_eq!(grid.width(), 40);
1157 assert_eq!(grid.height(), 20);
1158 assert_eq!(grid.dot_width(), 80);
1159 assert_eq!(grid.dot_height(), 80);
1160 }
1161
1162 #[test]
1163 fn test_new_minimal_dimensions() {
1164 let grid = BrailleGrid::new(1, 1);
1165 assert!(grid.is_ok());
1166 assert_eq!(grid.unwrap().dimensions(), (1, 1));
1167 }
1168
1169 #[test]
1170 fn test_new_large_dimensions() {
1171 let grid = BrailleGrid::new(200, 50);
1172 assert!(grid.is_ok());
1173 assert_eq!(grid.unwrap().dimensions(), (200, 50));
1174 }
1175
1176 #[test]
1177 fn test_new_max_dimensions() {
1178 let grid = BrailleGrid::new(10_000, 10_000);
1179 assert!(grid.is_ok());
1180 }
1181
1182 #[test]
1183 fn test_new_zero_width_returns_error() {
1184 let result = BrailleGrid::new(0, 24);
1185 assert!(matches!(
1186 result,
1187 Err(DotmaxError::InvalidDimensions {
1188 width: 0,
1189 height: 24
1190 })
1191 ));
1192 }
1193
1194 #[test]
1195 fn test_new_zero_height_returns_error() {
1196 let result = BrailleGrid::new(80, 0);
1197 assert!(matches!(
1198 result,
1199 Err(DotmaxError::InvalidDimensions {
1200 width: 80,
1201 height: 0
1202 })
1203 ));
1204 }
1205
1206 #[test]
1207 fn test_new_both_zero_returns_error() {
1208 let result = BrailleGrid::new(0, 0);
1209 assert!(matches!(
1210 result,
1211 Err(DotmaxError::InvalidDimensions {
1212 width: 0,
1213 height: 0
1214 })
1215 ));
1216 }
1217
1218 #[test]
1219 fn test_new_exceeds_max_width() {
1220 let result = BrailleGrid::new(10_001, 100);
1221 assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
1222 }
1223
1224 #[test]
1225 fn test_new_exceeds_max_height() {
1226 let result = BrailleGrid::new(100, 10_001);
1227 assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
1228 }
1229
1230 // ========================================================================
1231 // Dot Manipulation Tests (AC #3, #4, #8) - Adapted from crabmusic
1232 // ========================================================================
1233
1234 #[test]
1235 fn test_set_dot() {
1236 // Ported from crabmusic test_set_dot (line 398)
1237 let mut grid = BrailleGrid::new(10, 10).unwrap();
1238
1239 // Set top-left dot of first cell (dot coordinate 0,0)
1240 grid.set_dot(0, 0).unwrap();
1241 assert_eq!(grid.get_char(0, 0), '⠁');
1242
1243 // Set top-right dot of first cell (dot coordinate 1,0)
1244 grid.set_dot(1, 0).unwrap();
1245 assert_eq!(grid.get_char(0, 0), '⠉');
1246 }
1247
1248 #[test]
1249 fn test_dot_positions() {
1250 // Ported from crabmusic test_dot_positions (line 476)
1251 let mut grid = BrailleGrid::new(10, 10).unwrap();
1252
1253 // Test all 8 dot positions in first cell
1254 grid.clear();
1255 grid.set_dot(0, 0).unwrap(); // Dot 1
1256 assert_eq!(grid.patterns[0], 0b0000_0001);
1257
1258 grid.clear();
1259 grid.set_dot(0, 1).unwrap(); // Dot 2
1260 assert_eq!(grid.patterns[0], 0b0000_0010);
1261
1262 grid.clear();
1263 grid.set_dot(0, 2).unwrap(); // Dot 3
1264 assert_eq!(grid.patterns[0], 0b0000_0100);
1265
1266 grid.clear();
1267 grid.set_dot(0, 3).unwrap(); // Dot 7
1268 assert_eq!(grid.patterns[0], 0b0100_0000);
1269
1270 grid.clear();
1271 grid.set_dot(1, 0).unwrap(); // Dot 4
1272 assert_eq!(grid.patterns[0], 0b0000_1000);
1273
1274 grid.clear();
1275 grid.set_dot(1, 1).unwrap(); // Dot 5
1276 assert_eq!(grid.patterns[0], 0b0001_0000);
1277
1278 grid.clear();
1279 grid.set_dot(1, 2).unwrap(); // Dot 6
1280 assert_eq!(grid.patterns[0], 0b0010_0000);
1281
1282 grid.clear();
1283 grid.set_dot(1, 3).unwrap(); // Dot 8
1284 assert_eq!(grid.patterns[0], 0b1000_0000);
1285 }
1286
1287 #[test]
1288 fn test_get_dot_all_positions() {
1289 let mut grid = BrailleGrid::new(10, 10).unwrap();
1290
1291 // Set all 8 dots in cell (5,5) using set_dot pixel API
1292 for dot_y in 0..4 {
1293 for dot_x in 0..2 {
1294 // Cell (5,5) corresponds to dot range (10-11, 20-23)
1295 grid.set_dot(5 * 2 + dot_x, 5 * 4 + dot_y).unwrap();
1296 }
1297 }
1298
1299 // Verify all 8 dots are set using get_dot
1300 for dot_index in 0..8 {
1301 assert!(
1302 grid.get_dot(5, 5, dot_index).unwrap(),
1303 "Dot {dot_index} should be set"
1304 );
1305 }
1306 }
1307
1308 #[test]
1309 fn test_set_dot_out_of_bounds() {
1310 let mut grid = BrailleGrid::new(10, 10).unwrap();
1311 // Grid is 10×10 cells = 20×40 dots
1312 let result = grid.set_dot(100, 5);
1313 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1314 }
1315
1316 #[test]
1317 fn test_get_dot_out_of_bounds() {
1318 let grid = BrailleGrid::new(10, 10).unwrap();
1319 let result = grid.get_dot(100, 100, 0);
1320 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1321 }
1322
1323 #[test]
1324 fn test_get_dot_invalid_dot_index() {
1325 let grid = BrailleGrid::new(10, 10).unwrap();
1326 let result = grid.get_dot(5, 5, 8);
1327 assert!(matches!(result, Err(DotmaxError::InvalidDotIndex { .. })));
1328 }
1329
1330 #[test]
1331 fn test_bounds_checking() {
1332 // Ported from crabmusic test_bounds_checking (line 514)
1333 let mut grid = BrailleGrid::new(10, 10).unwrap();
1334
1335 // Should return error, not panic
1336 let result = grid.set_dot(1000, 1000);
1337 assert!(result.is_err());
1338
1339 assert_eq!(grid.get_char(1000, 1000), '⠀');
1340 assert!(grid.is_empty(1000, 1000));
1341 }
1342
1343 // ========================================================================
1344 // Clear Operations Tests (AC #5, #6) - Adapted from crabmusic
1345 // ========================================================================
1346
1347 #[test]
1348 fn test_clear() {
1349 // Ported from crabmusic test_clear (line 411)
1350 let mut grid = BrailleGrid::new(10, 10).unwrap();
1351
1352 grid.set_dot(0, 0).unwrap();
1353 grid.set_dot(5, 5).unwrap();
1354
1355 grid.clear();
1356
1357 assert_eq!(grid.get_char(0, 0), '⠀');
1358 assert!(grid.is_empty(0, 0));
1359 }
1360
1361 #[test]
1362 fn test_clear_empty_grid() {
1363 let mut grid = BrailleGrid::new(5, 5).unwrap();
1364 grid.clear(); // Should not panic on empty grid
1365 assert!(grid.is_empty(0, 0));
1366 }
1367
1368 #[test]
1369 fn test_clear_region_basic() {
1370 let mut grid = BrailleGrid::new(20, 20).unwrap();
1371
1372 // Set dots in various cells
1373 grid.set_dot(5 * 2, 5 * 4).unwrap(); // Cell (5,5)
1374 grid.set_dot(6 * 2, 6 * 4).unwrap(); // Cell (6,6)
1375 grid.set_dot(10 * 2, 10 * 4).unwrap(); // Cell (10,10)
1376
1377 // Clear region (5, 5, 2, 2) - clears cells (5,5), (6,5), (5,6), (6,6)
1378 grid.clear_region(5, 5, 2, 2).unwrap();
1379
1380 // Verify region cleared
1381 assert!(grid.is_empty(5, 5));
1382 assert!(grid.is_empty(6, 6));
1383
1384 // Verify outside region unchanged
1385 assert!(!grid.is_empty(10, 10));
1386 }
1387
1388 #[test]
1389 fn test_clear_region_out_of_bounds() {
1390 let mut grid = BrailleGrid::new(10, 10).unwrap();
1391
1392 // Region extends beyond grid
1393 let result = grid.clear_region(5, 5, 10, 10);
1394 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1395 }
1396
1397 #[test]
1398 fn test_clear_region_zero_size() {
1399 let mut grid = BrailleGrid::new(10, 10).unwrap();
1400
1401 // Zero-size region should succeed (clears nothing)
1402 let result = grid.clear_region(5, 5, 0, 0);
1403 assert!(result.is_ok());
1404 }
1405
1406 // ========================================================================
1407 // Dimensions Test (AC #7)
1408 // ========================================================================
1409
1410 #[test]
1411 fn test_dimensions_returns_correct_size() {
1412 let grid1 = BrailleGrid::new(80, 24).unwrap();
1413 assert_eq!(grid1.dimensions(), (80, 24));
1414
1415 let grid2 = BrailleGrid::new(100, 50).unwrap();
1416 assert_eq!(grid2.dimensions(), (100, 50));
1417
1418 let grid3 = BrailleGrid::new(1, 1).unwrap();
1419 assert_eq!(grid3.dimensions(), (1, 1));
1420 }
1421
1422 // ========================================================================
1423 // Unicode Conversion Tests - Ported from crabmusic
1424 // ========================================================================
1425
1426 #[test]
1427 fn test_dots_to_char() {
1428 // Ported from crabmusic test_dots_to_char (line 376)
1429 // Empty pattern
1430 assert_eq!(dots_to_char(0b0000_0000), '⠀');
1431
1432 // All dots
1433 assert_eq!(dots_to_char(0b1111_1111), '⣿');
1434
1435 // Single dots
1436 assert_eq!(dots_to_char(0b0000_0001), '⠁'); // Dot 1
1437 assert_eq!(dots_to_char(0b0000_1000), '⠈'); // Dot 4
1438 }
1439
1440 // ========================================================================
1441 // Color Tests (AC #1 - Color struct)
1442 // ========================================================================
1443
1444 #[test]
1445 fn test_color_rgb() {
1446 let color = Color::rgb(255, 128, 64);
1447 assert_eq!(color.r, 255);
1448 assert_eq!(color.g, 128);
1449 assert_eq!(color.b, 64);
1450 }
1451
1452 #[test]
1453 fn test_color_black() {
1454 let color = Color::black();
1455 assert_eq!(color.r, 0);
1456 assert_eq!(color.g, 0);
1457 assert_eq!(color.b, 0);
1458 }
1459
1460 #[test]
1461 fn test_color_white() {
1462 let color = Color::white();
1463 assert_eq!(color.r, 255);
1464 assert_eq!(color.g, 255);
1465 assert_eq!(color.b, 255);
1466 }
1467
1468 #[test]
1469 fn test_color_equality() {
1470 let color1 = Color::rgb(100, 150, 200);
1471 let color2 = Color::rgb(100, 150, 200);
1472 let color3 = Color::rgb(100, 150, 201);
1473
1474 assert_eq!(color1, color2);
1475 assert_ne!(color1, color3);
1476 }
1477
1478 // ========================================================================
1479 // Story 2.2: Unicode Braille Character Conversion Tests (AC #4, #5)
1480 // ========================================================================
1481
1482 /// Test all 256 braille patterns (exhaustive coverage for AC #4)
1483 ///
1484 /// This test verifies correctness of the Unicode Braille conversion
1485 /// for ALL possible 8-dot patterns (2^8 = 256 combinations).
1486 ///
1487 /// Tests the bitfield formula: U+2800 + (dots[0]<<0 | dots[1]<<1 | ... | dots[7]<<7)
1488 #[test]
1489 fn test_all_256_braille_patterns() {
1490 for bitfield in 0u8..=255 {
1491 let ch = dots_to_char(bitfield);
1492 let expected = char::from_u32(0x2800 + u32::from(bitfield)).unwrap();
1493 assert_eq!(
1494 ch, expected,
1495 "Failed for bitfield {bitfield:08b} (decimal {bitfield})"
1496 );
1497 }
1498 }
1499
1500 /// Test empty cell → U+2800 (AC #5)
1501 #[test]
1502 fn test_empty_cell_is_u2800() {
1503 let ch = dots_to_char(0b0000_0000);
1504 assert_eq!(ch, '\u{2800}', "Empty cell should be blank braille U+2800");
1505 }
1506
1507 /// Test full cell → U+28FF (AC #5)
1508 #[test]
1509 fn test_full_cell_is_u28ff() {
1510 let ch = dots_to_char(0b1111_1111);
1511 assert_eq!(ch, '\u{28FF}', "Full cell should be U+28FF (all dots)");
1512 }
1513
1514 /// Test specific patterns match Unicode standard (AC #5)
1515 #[test]
1516 fn test_specific_braille_patterns() {
1517 // Pattern: dots [true, false, true, false, false, false, false, false]
1518 // Bitfield: 0b00000101 = 5
1519 // Expected: U+2805 = '⠅'
1520 assert_eq!(dots_to_char(0b0000_0101), '\u{2805}');
1521
1522 // Pattern: dots [true, true, true, true, false, false, false, false]
1523 // Bitfield: 0b00001111 = 15
1524 // Expected: U+280F = '⠏'
1525 assert_eq!(dots_to_char(0b0000_1111), '\u{280F}');
1526
1527 // Single dot patterns
1528 assert_eq!(dots_to_char(0b0000_0001), '\u{2801}'); // Dot 1 only
1529 assert_eq!(dots_to_char(0b0000_1000), '\u{2808}'); // Dot 4 only
1530 assert_eq!(dots_to_char(0b0100_0000), '\u{2840}'); // Dot 7 only
1531 assert_eq!(dots_to_char(0b1000_0000), '\u{2880}'); // Dot 8 only
1532 }
1533
1534 /// Test `to_unicode_grid()` dimensions (AC #2)
1535 #[test]
1536 fn test_to_unicode_grid_dimensions() {
1537 // 5×5 grid → verify result is 5×5 Vec<Vec<char>>
1538 let grid = BrailleGrid::new(5, 5).unwrap();
1539 let chars = grid.to_unicode_grid();
1540
1541 assert_eq!(chars.len(), 5, "Grid should have 5 rows");
1542 for row in &chars {
1543 assert_eq!(row.len(), 5, "Each row should have 5 columns");
1544 }
1545 }
1546
1547 /// Test `to_unicode_grid()` with various grid sizes
1548 #[test]
1549 fn test_to_unicode_grid_various_sizes() {
1550 // 80×24 (standard terminal)
1551 let grid1 = BrailleGrid::new(80, 24).unwrap();
1552 let chars1 = grid1.to_unicode_grid();
1553 assert_eq!(chars1.len(), 24);
1554 assert_eq!(chars1[0].len(), 80);
1555
1556 // 1×1 (minimal)
1557 let grid2 = BrailleGrid::new(1, 1).unwrap();
1558 let chars2 = grid2.to_unicode_grid();
1559 assert_eq!(chars2.len(), 1);
1560 assert_eq!(chars2[0].len(), 1);
1561
1562 // 100×50 (large terminal)
1563 let grid3 = BrailleGrid::new(100, 50).unwrap();
1564 let chars3 = grid3.to_unicode_grid();
1565 assert_eq!(chars3.len(), 50);
1566 assert_eq!(chars3[0].len(), 100);
1567 }
1568
1569 /// Test `to_unicode_grid()` with empty grid (all blank braille)
1570 #[test]
1571 fn test_to_unicode_grid_empty() {
1572 let grid = BrailleGrid::new(3, 3).unwrap();
1573 let chars = grid.to_unicode_grid();
1574
1575 // All cells should be blank braille U+2800
1576 for row in chars {
1577 for ch in row {
1578 assert_eq!(ch, '\u{2800}', "Empty grid should have blank braille");
1579 }
1580 }
1581 }
1582
1583 /// Test `to_unicode_grid()` with dots set
1584 #[test]
1585 fn test_to_unicode_grid_with_dots() {
1586 let mut grid = BrailleGrid::new(5, 5).unwrap();
1587
1588 // Set top-left dot of cell (0,0)
1589 grid.set_dot(0, 0).unwrap();
1590
1591 // Set all dots of cell (2,2)
1592 for dot_y in 0..4 {
1593 for dot_x in 0..2 {
1594 grid.set_dot(2 * 2 + dot_x, 2 * 4 + dot_y).unwrap();
1595 }
1596 }
1597
1598 let chars = grid.to_unicode_grid();
1599
1600 // Cell (0,0) should have dot 1 set → '⠁'
1601 assert_eq!(chars[0][0], '\u{2801}');
1602
1603 // Cell (2,2) should have all dots → '⣿'
1604 assert_eq!(chars[2][2], '\u{28FF}');
1605
1606 // Other cells should be blank
1607 assert_eq!(chars[1][1], '\u{2800}');
1608 assert_eq!(chars[4][4], '\u{2800}');
1609 }
1610
1611 /// Test `cell_to_braille_char()` bounds validation (AC #3)
1612 #[test]
1613 fn test_cell_to_braille_char_out_of_bounds() {
1614 let grid = BrailleGrid::new(10, 10).unwrap();
1615
1616 // Out of bounds → Err(OutOfBounds)
1617 let result1 = grid.cell_to_braille_char(100, 5);
1618 assert!(matches!(result1, Err(DotmaxError::OutOfBounds { .. })));
1619
1620 let result2 = grid.cell_to_braille_char(5, 100);
1621 assert!(matches!(result2, Err(DotmaxError::OutOfBounds { .. })));
1622
1623 let result3 = grid.cell_to_braille_char(10, 10); // Exactly at boundary
1624 assert!(matches!(result3, Err(DotmaxError::OutOfBounds { .. })));
1625 }
1626
1627 /// Test `cell_to_braille_char()` returns correct character
1628 #[test]
1629 fn test_cell_to_braille_char_correct_conversion() {
1630 let mut grid = BrailleGrid::new(10, 10).unwrap();
1631
1632 // Set specific pattern in cell (5,5)
1633 // Set dots to create pattern 0b00001111 (bitfield 15) → '⠏'
1634 grid.set_dot(5 * 2, 5 * 4).unwrap(); // Dot 1
1635 grid.set_dot(5 * 2, 5 * 4 + 1).unwrap(); // Dot 2
1636 grid.set_dot(5 * 2, 5 * 4 + 2).unwrap(); // Dot 3
1637 grid.set_dot(5 * 2 + 1, 5 * 4).unwrap(); // Dot 4
1638
1639 let ch = grid.cell_to_braille_char(5, 5).unwrap();
1640 assert_eq!(ch, '\u{280F}');
1641 }
1642
1643 /// Test `cell_to_braille_char()` for empty cells
1644 #[test]
1645 fn test_cell_to_braille_char_empty_cells() {
1646 let grid = BrailleGrid::new(10, 10).unwrap();
1647
1648 // All cells should start as blank braille
1649 for y in 0..10 {
1650 for x in 0..10 {
1651 let ch = grid.cell_to_braille_char(x, y).unwrap();
1652 assert_eq!(ch, '\u{2800}', "Empty cell ({x}, {y}) should be blank");
1653 }
1654 }
1655 }
1656
1657 /// Test that conversion is correct after clearing
1658 #[test]
1659 fn test_unicode_conversion_after_clear() {
1660 let mut grid = BrailleGrid::new(5, 5).unwrap();
1661
1662 // Set some dots
1663 grid.set_dot(0, 0).unwrap();
1664 grid.set_dot(5, 5).unwrap();
1665
1666 // Verify they're set
1667 assert_ne!(grid.cell_to_braille_char(0, 0).unwrap(), '\u{2800}');
1668
1669 // Clear grid
1670 grid.clear();
1671
1672 // Verify all cells are blank braille
1673 let chars = grid.to_unicode_grid();
1674 for row in chars {
1675 for ch in row {
1676 assert_eq!(ch, '\u{2800}');
1677 }
1678 }
1679 }
1680
1681 /// Test Unicode range validity (all conversions produce valid braille)
1682 #[test]
1683 fn test_unicode_range_validity() {
1684 let grid = BrailleGrid::new(5, 5).unwrap();
1685 let chars = grid.to_unicode_grid();
1686
1687 for row in chars {
1688 for ch in row {
1689 assert!(
1690 ('\u{2800}'..='\u{28FF}').contains(&ch),
1691 "Character {ch} is outside braille range U+2800-U+28FF"
1692 );
1693 }
1694 }
1695 }
1696
1697 // ========================================================================
1698 // Story 2.4: Error Context Verification Tests (AC #3)
1699 // ========================================================================
1700
1701 /// Test `InvalidDimensions` error message includes context (AC #3)
1702 #[test]
1703 fn test_invalid_dimensions_error_message_includes_context() {
1704 let result = BrailleGrid::new(0, 10);
1705 match result {
1706 Err(DotmaxError::InvalidDimensions { width, height }) => {
1707 let msg = format!("{}", DotmaxError::InvalidDimensions { width, height });
1708 assert!(msg.contains('0'), "Error message should include width=0");
1709 assert!(msg.contains("10"), "Error message should include height=10");
1710 assert!(
1711 msg.contains("width") && msg.contains("height"),
1712 "Error message should label dimensions"
1713 );
1714 }
1715 _ => panic!("Expected InvalidDimensions error"),
1716 }
1717 }
1718
1719 /// Test `OutOfBounds` error message includes all context (AC #3)
1720 #[test]
1721 fn test_out_of_bounds_error_message_includes_all_context() {
1722 let mut grid = BrailleGrid::new(10, 10).unwrap();
1723 let result = grid.set_dot(100, 50);
1724 match result {
1725 Err(DotmaxError::OutOfBounds {
1726 x,
1727 y,
1728 width,
1729 height,
1730 }) => {
1731 let msg = format!(
1732 "{}",
1733 DotmaxError::OutOfBounds {
1734 x,
1735 y,
1736 width,
1737 height
1738 }
1739 );
1740 assert!(msg.contains("100"), "Error message should include x=100");
1741 assert!(msg.contains("50"), "Error message should include y=50");
1742 // width*2=20 and height*4=40 for dot coordinates
1743 assert!(
1744 msg.contains("20"),
1745 "Error message should include dot_width=20"
1746 );
1747 assert!(
1748 msg.contains("40"),
1749 "Error message should include dot_height=40"
1750 );
1751 }
1752 _ => panic!("Expected OutOfBounds error"),
1753 }
1754 }
1755
1756 /// Test `InvalidDotIndex` error message includes index (AC #3)
1757 #[test]
1758 fn test_invalid_dot_index_error_message_includes_index() {
1759 let grid = BrailleGrid::new(10, 10).unwrap();
1760 let result = grid.get_dot(5, 5, 10);
1761 match result {
1762 Err(DotmaxError::InvalidDotIndex { index }) => {
1763 let msg = format!("{}", DotmaxError::InvalidDotIndex { index });
1764 assert!(msg.contains("10"), "Error message should include index=10");
1765 assert!(
1766 msg.contains("0-7"),
1767 "Error message should specify valid range"
1768 );
1769 }
1770 _ => panic!("Expected InvalidDotIndex error"),
1771 }
1772 }
1773
1774 /// Test exceeding maximum dimensions returns proper error (AC #1, #3)
1775 #[test]
1776 fn test_new_exceeds_both_max_dimensions() {
1777 let result = BrailleGrid::new(20_000, 20_000);
1778 assert!(
1779 matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1780 "Grid exceeding MAX_GRID_WIDTH and MAX_GRID_HEIGHT should return InvalidDimensions"
1781 );
1782 }
1783
1784 /// Test `set_dot` with invalid dot index returns `InvalidDotIndex` (AC #1)
1785 #[test]
1786 fn test_set_dot_invalid_dot_index_high() {
1787 let grid = BrailleGrid::new(10, 10).unwrap();
1788 // set_dot uses dot coordinates, not dot_index, so we test via get_dot
1789 let result = grid.get_dot(5, 5, 255);
1790 assert!(
1791 matches!(result, Err(DotmaxError::InvalidDotIndex { index: 255 })),
1792 "Dot index 255 should return InvalidDotIndex error"
1793 );
1794 }
1795
1796 // ========================================================================
1797 // Story 2.5: Terminal Resize Event Handling Tests
1798 // ========================================================================
1799
1800 /// Test resize grow updates dimensions (AC #2, #3)
1801 #[test]
1802 fn test_resize_grow_updates_dimensions() {
1803 let mut grid = BrailleGrid::new(10, 10).unwrap();
1804 grid.resize(20, 20).unwrap();
1805 assert_eq!(grid.dimensions(), (20, 20));
1806 assert_eq!(grid.width(), 20);
1807 assert_eq!(grid.height(), 20);
1808 }
1809
1810 /// Test resize grow preserves existing dots (AC #3)
1811 #[test]
1812 fn test_resize_grow_preserves_existing_dots() {
1813 let mut grid = BrailleGrid::new(10, 10).unwrap();
1814 grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1815 grid.set_dot(18, 38).unwrap(); // Sets bit in cell (9, 9)
1816
1817 // Check initial state
1818 let cell_2_1 = 10 + 2; // cell (2, 1) = index 12
1819 let cell_9_9 = 9 * 10 + 9; // cell (9, 9) = index 99
1820 assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
1821 assert_ne!(grid.patterns[cell_9_9], 0, "Cell (9,9) should have dots");
1822
1823 grid.resize(20, 20).unwrap();
1824
1825 // After resize, cells should be at same logical positions
1826 let new_cell_2_1 = 20 + 2; // cell (2, 1) in new grid
1827 let new_cell_9_9 = 9 * 20 + 9; // cell (9, 9) in new grid
1828
1829 // Existing dots should be preserved
1830 assert_ne!(
1831 grid.patterns[new_cell_2_1], 0,
1832 "Cell (2,1) dots should be preserved"
1833 );
1834 assert_ne!(
1835 grid.patterns[new_cell_9_9], 0,
1836 "Cell (9,9) dots should be preserved"
1837 );
1838
1839 // New cells should be empty
1840 let new_cell_15_15 = 15 * 20 + 15;
1841 assert_eq!(
1842 grid.patterns[new_cell_15_15], 0,
1843 "New cells should be empty"
1844 );
1845 }
1846
1847 /// Test resize shrink truncates cleanly (AC #4)
1848 #[test]
1849 fn test_resize_shrink_truncates_cleanly() {
1850 let mut grid = BrailleGrid::new(20, 20).unwrap();
1851 grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1852 grid.set_dot(30, 60).unwrap(); // Sets bit in cell (15, 15) - will be truncated
1853
1854 // Check initial state
1855 let cell_2_1 = 20 + 2;
1856 let cell_15_15 = 15 * 20 + 15;
1857 assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
1858 assert_ne!(
1859 grid.patterns[cell_15_15], 0,
1860 "Cell (15,15) should have dots"
1861 );
1862
1863 grid.resize(10, 10).unwrap();
1864
1865 assert_eq!(grid.dimensions(), (10, 10));
1866
1867 // Preserved dot should still exist
1868 let new_cell_2_1 = 10 + 2;
1869 assert_ne!(
1870 grid.patterns[new_cell_2_1], 0,
1871 "Cell (2,1) dots should be preserved"
1872 );
1873
1874 // Grid is now only 10×10 = 100 cells, so cell (15,15) is truncated
1875 assert_eq!(
1876 grid.patterns.len(),
1877 100,
1878 "Grid should have only 100 cells after resize to 10×10"
1879 );
1880 }
1881
1882 /// Test resize to same dimensions (no-op case)
1883 #[test]
1884 fn test_resize_same_dimensions() {
1885 let mut grid = BrailleGrid::new(10, 10).unwrap();
1886 grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1887
1888 let cell_2_1 = 10 + 2;
1889 let original_pattern = grid.patterns[cell_2_1];
1890 assert_ne!(original_pattern, 0);
1891
1892 grid.resize(10, 10).unwrap();
1893
1894 assert_eq!(grid.dimensions(), (10, 10));
1895 // Pattern should be unchanged
1896 assert_eq!(
1897 grid.patterns[cell_2_1], original_pattern,
1898 "Existing dot pattern should be preserved"
1899 );
1900 }
1901
1902 /// Test resize with colors syncs color buffer (AC #5)
1903 #[test]
1904 fn test_resize_with_colors_syncs_color_buffer() {
1905 let mut grid = BrailleGrid::new(10, 10).unwrap();
1906 // Note: Color support is implicit in BrailleGrid (colors vec always allocated)
1907 // We can test via internal state
1908
1909 grid.resize(20, 20).unwrap();
1910
1911 // Color buffer should have resized to match new dimensions
1912 assert_eq!(
1913 grid.colors.len(),
1914 400,
1915 "Color buffer should have 400 cells for 20×20 grid"
1916 );
1917 }
1918
1919 /// Test resize zero width dimension error (AC #2)
1920 #[test]
1921 fn test_resize_zero_width_error() {
1922 let mut grid = BrailleGrid::new(10, 10).unwrap();
1923 let result = grid.resize(0, 10);
1924 assert!(
1925 matches!(
1926 result,
1927 Err(DotmaxError::InvalidDimensions {
1928 width: 0,
1929 height: 10
1930 })
1931 ),
1932 "Resize to width=0 should return InvalidDimensions error"
1933 );
1934 // Grid dimensions should remain unchanged after failed resize
1935 assert_eq!(grid.dimensions(), (10, 10));
1936 }
1937
1938 /// Test resize zero height dimension error (AC #2)
1939 #[test]
1940 fn test_resize_zero_height_error() {
1941 let mut grid = BrailleGrid::new(10, 10).unwrap();
1942 let result = grid.resize(10, 0);
1943 assert!(
1944 matches!(
1945 result,
1946 Err(DotmaxError::InvalidDimensions {
1947 width: 10,
1948 height: 0
1949 })
1950 ),
1951 "Resize to height=0 should return InvalidDimensions error"
1952 );
1953 assert_eq!(grid.dimensions(), (10, 10));
1954 }
1955
1956 /// Test resize exceeds max width (AC #2)
1957 #[test]
1958 fn test_resize_exceeds_max_width_error() {
1959 let mut grid = BrailleGrid::new(10, 10).unwrap();
1960 let result = grid.resize(20000, 10);
1961 assert!(
1962 matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1963 "Resize to width=20000 should return InvalidDimensions error"
1964 );
1965 assert_eq!(grid.dimensions(), (10, 10));
1966 }
1967
1968 /// Test resize exceeds max height (AC #2)
1969 #[test]
1970 fn test_resize_exceeds_max_height_error() {
1971 let mut grid = BrailleGrid::new(10, 10).unwrap();
1972 let result = grid.resize(10, 20000);
1973 assert!(
1974 matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1975 "Resize to height=20000 should return InvalidDimensions error"
1976 );
1977 assert_eq!(grid.dimensions(), (10, 10));
1978 }
1979
1980 /// Test resize maintains grid invariants (AC #6)
1981 #[test]
1982 fn test_resize_maintains_invariants() {
1983 let mut grid = BrailleGrid::new(10, 10).unwrap();
1984 grid.resize(20, 15).unwrap();
1985
1986 // Verify invariants
1987 assert_eq!(
1988 grid.patterns.len(),
1989 300,
1990 "Patterns buffer should have 300 cells for 20×15 grid"
1991 );
1992 assert_eq!(
1993 grid.colors.len(),
1994 300,
1995 "Colors buffer should have 300 cells for 20×15 grid"
1996 );
1997 assert_eq!(grid.width(), 20);
1998 assert_eq!(grid.height(), 15);
1999 }
2000
2001 /// Test resize from 1×1 to large grid (edge case)
2002 #[test]
2003 fn test_resize_from_tiny_to_large() {
2004 let mut grid = BrailleGrid::new(1, 1).unwrap();
2005 grid.set_dot(0, 0).unwrap();
2006
2007 grid.resize(50, 30).unwrap();
2008
2009 assert_eq!(grid.dimensions(), (50, 30));
2010 assert!(
2011 grid.get_dot(0, 0, 0).unwrap(),
2012 "Single dot should be preserved at (0,0)"
2013 );
2014 assert!(
2015 !grid.get_dot(10, 10, 0).unwrap(),
2016 "New cells should be empty"
2017 );
2018 }
2019
2020 /// Test resize shrink to 1×1 (edge case)
2021 #[test]
2022 fn test_resize_shrink_to_tiny() {
2023 let mut grid = BrailleGrid::new(50, 30).unwrap();
2024 grid.set_dot(0, 0).unwrap();
2025 grid.set_dot(10, 10).unwrap();
2026
2027 grid.resize(1, 1).unwrap();
2028
2029 assert_eq!(grid.dimensions(), (1, 1));
2030 assert!(
2031 grid.get_dot(0, 0, 0).unwrap(),
2032 "Top-left dot should be preserved"
2033 );
2034 // Other dots are now out of bounds
2035 assert!(grid.get_dot(10, 10, 0).is_err());
2036 }
2037
2038 // ========================================================================
2039 // Story 2.6: Color Support Tests (AC #1-#7)
2040 // ========================================================================
2041
2042 /// Test `enable_color_support()` allocates buffer (AC #3)
2043 #[test]
2044 fn test_enable_color_support_allocates_buffer() {
2045 let mut grid = BrailleGrid::new(10, 10).unwrap();
2046 // Color buffer is already allocated in new(), so this is a no-op
2047 // But we verify it exists and has correct size
2048 grid.enable_color_support();
2049
2050 // Verify buffer size matches grid dimensions
2051 assert_eq!(grid.colors.len(), 100); // 10×10 = 100 cells
2052 }
2053
2054 /// Test `set_cell_color()` assigns color (AC #4)
2055 #[test]
2056 fn test_set_cell_color_assigns_color() {
2057 let mut grid = BrailleGrid::new(10, 10).unwrap();
2058 grid.enable_color_support();
2059
2060 let red = Color::rgb(255, 0, 0);
2061 grid.set_cell_color(5, 5, red).unwrap();
2062
2063 assert_eq!(grid.get_color(5, 5), Some(red));
2064 }
2065
2066 /// Test `set_cell_color()` with valid coordinates (AC #4)
2067 #[test]
2068 fn test_set_cell_color_valid_coordinates() {
2069 let mut grid = BrailleGrid::new(10, 10).unwrap();
2070 grid.enable_color_support();
2071
2072 // Test corners and center
2073 let blue = Color::rgb(0, 0, 255);
2074 grid.set_cell_color(0, 0, blue).unwrap();
2075 assert_eq!(grid.get_color(0, 0), Some(blue));
2076
2077 let green = Color::rgb(0, 255, 0);
2078 grid.set_cell_color(9, 9, green).unwrap();
2079 assert_eq!(grid.get_color(9, 9), Some(green));
2080
2081 let yellow = Color::rgb(255, 255, 0);
2082 grid.set_cell_color(5, 5, yellow).unwrap();
2083 assert_eq!(grid.get_color(5, 5), Some(yellow));
2084 }
2085
2086 /// Test `set_cell_color()` out of bounds returns error (AC #4)
2087 #[test]
2088 fn test_set_cell_color_out_of_bounds_error() {
2089 let mut grid = BrailleGrid::new(10, 10).unwrap();
2090 grid.enable_color_support();
2091
2092 let result = grid.set_cell_color(100, 100, Color::black());
2093 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2094 }
2095
2096 /// Test `set_cell_color()` out of bounds X (AC #4)
2097 #[test]
2098 fn test_set_cell_color_out_of_bounds_x() {
2099 let mut grid = BrailleGrid::new(10, 10).unwrap();
2100 grid.enable_color_support();
2101
2102 let result = grid.set_cell_color(10, 5, Color::white());
2103 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2104 }
2105
2106 /// Test `set_cell_color()` out of bounds Y (AC #4)
2107 #[test]
2108 fn test_set_cell_color_out_of_bounds_y() {
2109 let mut grid = BrailleGrid::new(10, 10).unwrap();
2110 grid.enable_color_support();
2111
2112 let result = grid.set_cell_color(5, 10, Color::white());
2113 assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2114 }
2115
2116 /// Test `get_color()` returns None when no color set (AC #5)
2117 #[test]
2118 fn test_get_color_none_when_not_set() {
2119 let mut grid = BrailleGrid::new(10, 10).unwrap();
2120 grid.enable_color_support();
2121
2122 // No color set on cell (5, 5)
2123 assert_eq!(grid.get_color(5, 5), None);
2124 }
2125
2126 /// Test `get_color()` returns None for out of bounds (AC #5)
2127 #[test]
2128 fn test_get_color_none_when_out_of_bounds() {
2129 let grid = BrailleGrid::new(10, 10).unwrap();
2130
2131 // Out of bounds returns None (not error)
2132 assert_eq!(grid.get_color(100, 100), None);
2133 }
2134
2135 /// Test `get_color()` returns color after set (AC #5)
2136 #[test]
2137 fn test_get_color_returns_color_after_set() {
2138 let mut grid = BrailleGrid::new(10, 10).unwrap();
2139 grid.enable_color_support();
2140
2141 let magenta = Color::rgb(255, 0, 255);
2142 grid.set_cell_color(7, 7, magenta).unwrap();
2143
2144 assert_eq!(grid.get_color(7, 7), Some(magenta));
2145 }
2146
2147 /// Test `clear_colors()` resets all colors to None (AC #7)
2148 #[test]
2149 fn test_clear_colors_resets_all() {
2150 let mut grid = BrailleGrid::new(10, 10).unwrap();
2151 grid.enable_color_support();
2152
2153 // Set colors on multiple cells
2154 grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2155 grid.set_cell_color(7, 7, Color::rgb(0, 255, 0)).unwrap();
2156 grid.set_cell_color(2, 2, Color::rgb(0, 0, 255)).unwrap();
2157
2158 // Verify colors are set
2159 assert!(grid.get_color(5, 5).is_some());
2160 assert!(grid.get_color(7, 7).is_some());
2161 assert!(grid.get_color(2, 2).is_some());
2162
2163 // Clear all colors
2164 grid.clear_colors();
2165
2166 // All colors should be None
2167 assert_eq!(grid.get_color(5, 5), None);
2168 assert_eq!(grid.get_color(7, 7), None);
2169 assert_eq!(grid.get_color(2, 2), None);
2170 }
2171
2172 /// Test `clear_colors()` doesn't affect dots (AC #7)
2173 #[test]
2174 fn test_clear_colors_preserves_dots() {
2175 let mut grid = BrailleGrid::new(10, 10).unwrap();
2176 grid.enable_color_support();
2177
2178 // Set dots and colors
2179 grid.set_dot(10, 20).unwrap();
2180 grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2181
2182 // Clear colors
2183 grid.clear_colors();
2184
2185 // Dots should still exist
2186 assert!(!grid.is_empty(5, 5));
2187 // But colors should be None
2188 assert_eq!(grid.get_color(5, 5), None);
2189 }
2190
2191 /// Test Color `PartialEq` works correctly (AC #7)
2192 #[test]
2193 fn test_color_partial_eq() {
2194 let red1 = Color::rgb(255, 0, 0);
2195 let red2 = Color::rgb(255, 0, 0);
2196 let blue = Color::rgb(0, 0, 255);
2197
2198 assert_eq!(red1, red2);
2199 assert_ne!(red1, blue);
2200 }
2201
2202 /// Test colors persist after resize (AC #5, Story 2.5 AC #5)
2203 #[test]
2204 fn test_colors_persist_after_resize_grow() {
2205 let mut grid = BrailleGrid::new(10, 10).unwrap();
2206 grid.enable_color_support();
2207
2208 let purple = Color::rgb(128, 0, 128);
2209 grid.set_cell_color(5, 5, purple).unwrap();
2210
2211 // Resize to larger
2212 grid.resize(20, 20).unwrap();
2213
2214 // Color should be preserved at same logical position
2215 assert_eq!(grid.get_color(5, 5), Some(purple));
2216
2217 // New cells should have no color
2218 assert_eq!(grid.get_color(15, 15), None);
2219 }
2220
2221 /// Test colors truncated correctly after resize shrink (AC #5, Story 2.5 AC #4)
2222 #[test]
2223 fn test_colors_truncated_after_resize_shrink() {
2224 let mut grid = BrailleGrid::new(20, 20).unwrap();
2225 grid.enable_color_support();
2226
2227 let orange = Color::rgb(255, 165, 0);
2228 grid.set_cell_color(5, 5, orange).unwrap();
2229 grid.set_cell_color(15, 15, Color::rgb(0, 255, 255))
2230 .unwrap();
2231
2232 // Resize to smaller
2233 grid.resize(10, 10).unwrap();
2234
2235 // Color within bounds should be preserved
2236 assert_eq!(grid.get_color(5, 5), Some(orange));
2237
2238 // Cell (15, 15) is now out of bounds
2239 assert_eq!(grid.get_color(15, 15), None);
2240 }
2241
2242 /// Test `enable_color_support()` is idempotent (AC #3)
2243 #[test]
2244 fn test_enable_color_support_idempotent() {
2245 let mut grid = BrailleGrid::new(10, 10).unwrap();
2246
2247 grid.enable_color_support();
2248 grid.enable_color_support();
2249 grid.enable_color_support();
2250
2251 // Should not panic or cause issues
2252 assert_eq!(grid.colors.len(), 100);
2253 }
2254
2255 /// Test `set_cell_color()` with all predefined colors (AC #2, #4)
2256 #[test]
2257 fn test_set_cell_color_with_predefined_colors() {
2258 let mut grid = BrailleGrid::new(10, 10).unwrap();
2259 grid.enable_color_support();
2260
2261 // Test black() constructor
2262 grid.set_cell_color(0, 0, Color::black()).unwrap();
2263 assert_eq!(grid.get_color(0, 0), Some(Color::rgb(0, 0, 0)));
2264
2265 // Test white() constructor
2266 grid.set_cell_color(1, 1, Color::white()).unwrap();
2267 assert_eq!(grid.get_color(1, 1), Some(Color::rgb(255, 255, 255)));
2268
2269 // Test rgb() constructor
2270 grid.set_cell_color(2, 2, Color::rgb(128, 64, 32)).unwrap();
2271 assert_eq!(grid.get_color(2, 2), Some(Color::rgb(128, 64, 32)));
2272 }
2273
2274 /// Test `clear()` also clears colors (not just `clear_colors()`)
2275 #[test]
2276 fn test_clear_also_clears_colors() {
2277 let mut grid = BrailleGrid::new(10, 10).unwrap();
2278 grid.enable_color_support();
2279
2280 grid.set_dot(10, 20).unwrap();
2281 grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2282
2283 grid.clear();
2284
2285 // Both dots and colors should be cleared
2286 assert!(grid.is_empty(5, 5));
2287 assert_eq!(grid.get_color(5, 5), None);
2288 }
2289
2290 /// Test resize preserves multiple dots in complex pattern (AC #3, #4)
2291 #[test]
2292 fn test_resize_preserves_complex_pattern() {
2293 let mut grid = BrailleGrid::new(15, 15).unwrap();
2294 // Set dots in a diagonal pattern
2295 // dot (0,0) → cell (0,0), dot (2,4) → cell (1,1), dot (4,8) → cell (2,2), etc.
2296 for i in 0..10 {
2297 grid.set_dot(i * 2, i * 4).unwrap();
2298 }
2299
2300 // Store original patterns for cells we'll verify
2301 let mut original_patterns = Vec::new();
2302 for i in 0..10 {
2303 let cell_x = (i * 2) / 2;
2304 let cell_y = (i * 4) / 4;
2305 let cell_index = cell_y * 15 + cell_x;
2306 original_patterns.push((cell_x, cell_y, grid.patterns[cell_index]));
2307 }
2308
2309 // Resize to larger
2310 grid.resize(20, 20).unwrap();
2311
2312 // All original diagonal dots should be preserved
2313 for (cell_x, cell_y, pattern) in &original_patterns {
2314 let new_index = cell_y * 20 + cell_x;
2315 assert_eq!(
2316 grid.patterns[new_index], *pattern,
2317 "Cell ({cell_x}, {cell_y}) pattern should be preserved after grow"
2318 );
2319 }
2320
2321 // Resize to smaller (truncate some dots)
2322 grid.resize(8, 8).unwrap();
2323
2324 // Dots within new bounds should be preserved (cells 0-7 in both dimensions)
2325 for (cell_x, cell_y, pattern) in &original_patterns {
2326 if *cell_x < 8 && *cell_y < 8 {
2327 let new_index = cell_y * 8 + cell_x;
2328 assert_eq!(
2329 grid.patterns[new_index], *pattern,
2330 "Cell ({cell_x}, {cell_y}) pattern should be preserved after shrink"
2331 );
2332 }
2333 }
2334 }
2335
2336 // ========================================================================
2337 // Story 2.7: Debug Logging and Tracing Tests (AC #1-#6)
2338 // ========================================================================
2339
2340 /// Test that tracing instrumentation compiles (#[instrument] attributes work)
2341 /// AC 2.7.2: Verify #[instrument] on key functions compiles without type errors
2342 #[test]
2343 fn test_instrumentation_compiles() {
2344 // This test verifies that #[instrument] attributes don't cause compilation errors
2345 // If this test compiles and runs, the instrumentation is correct
2346 let grid = BrailleGrid::new(10, 10);
2347 assert!(grid.is_ok());
2348 }
2349
2350 /// Test logging works when subscriber initialized
2351 /// AC 2.7.6: Tests can enable logging via tracing-subscriber
2352 #[test]
2353 fn test_logging_with_subscriber_initialized() {
2354 // Initialize subscriber for this test
2355 // Using try_init() to handle case where subscriber already initialized by other tests
2356 let _ = tracing_subscriber::fmt()
2357 .with_max_level(tracing::Level::DEBUG)
2358 .with_test_writer()
2359 .try_init();
2360
2361 // Operations should now log (logs will appear in test output with --nocapture)
2362 let mut grid = BrailleGrid::new(10, 10).unwrap();
2363 grid.clear();
2364 grid.resize(20, 20).unwrap();
2365 grid.enable_color_support();
2366
2367 // If this completes without panic, logging infrastructure works
2368 assert_eq!(grid.dimensions(), (20, 20));
2369 }
2370
2371 /// Test logging is silent when subscriber NOT initialized (zero-cost)
2372 /// AC 2.7.5: Library does not initialize subscriber (user controls logging)
2373 #[test]
2374 fn test_logging_silent_by_default() {
2375 // No subscriber initialized
2376 // Operations should complete without logging (zero-cost)
2377 let mut grid = BrailleGrid::new(10, 10).unwrap();
2378 grid.clear();
2379
2380 // This should work silently - no logs appear because no subscriber
2381 assert_eq!(grid.dimensions(), (10, 10));
2382 }
2383
2384 /// Test that hot paths have NO debug logs
2385 /// AC 2.7.4: `set_dot` and `get_dot` do NOT log at debug level
2386 #[test]
2387 fn test_hot_paths_no_debug_logs() {
2388 // set_dot and get_dot should NOT have debug! calls (only trace! if needed)
2389 // This is a code review check - the test verifies they work without performance impact
2390
2391 let mut grid = BrailleGrid::new(100, 100).unwrap();
2392
2393 // Call hot paths many times - should complete quickly
2394 for y in 0..200 {
2395 for x in 0..200 {
2396 // This should be fast - no debug logging overhead
2397 let _ = grid.set_dot(x, y);
2398 }
2399 }
2400
2401 // Verify operations completed
2402 assert_eq!(grid.dimensions(), (100, 100));
2403 }
2404
2405 /// Test error logging includes context
2406 /// AC 2.7.3: error! logs include actionable context (coordinates, dimensions)
2407 #[test]
2408 fn test_error_logging_includes_context() {
2409 let mut grid = BrailleGrid::new(10, 10).unwrap();
2410
2411 // These operations will emit error! logs with context
2412 let result1 = BrailleGrid::new(0, 0);
2413 assert!(result1.is_err());
2414
2415 let result2 = grid.set_dot(1000, 1000);
2416 assert!(result2.is_err());
2417
2418 let result3 = grid.set_cell_color(1000, 1000, Color::black());
2419 assert!(result3.is_err());
2420
2421 // If we reach here, error paths executed correctly
2422 // (Logs will show context if subscriber is initialized)
2423 }
2424
2425 /// Test instrumented functions return correct types
2426 /// AC 2.7.2: Verify #[instrument] doesn't break function signatures
2427 #[test]
2428 fn test_instrumented_functions_correct_types() {
2429 // Test that #[instrument] doesn't change function return types
2430 let result1: Result<BrailleGrid, DotmaxError> = BrailleGrid::new(10, 10);
2431 assert!(result1.is_ok());
2432
2433 let mut grid = result1.unwrap();
2434
2435 let result2: () = grid.clear();
2436 assert_eq!(result2, ());
2437
2438 let result3: Result<(), DotmaxError> = grid.resize(20, 20);
2439 assert!(result3.is_ok());
2440
2441 let result4: () = grid.enable_color_support();
2442 assert_eq!(result4, ());
2443 }
2444
2445 /// Test logging works in complex workflow
2446 /// AC 2.7.6: Verify logging throughout full workflow
2447 #[test]
2448 fn test_logging_in_full_workflow() {
2449 // Initialize subscriber
2450 let _ = tracing_subscriber::fmt()
2451 .with_max_level(tracing::Level::DEBUG)
2452 .with_test_writer()
2453 .try_init();
2454
2455 // Complex workflow that exercises all instrumented paths
2456 let mut grid = BrailleGrid::new(20, 20).unwrap();
2457 grid.enable_color_support();
2458
2459 // Set dots
2460 grid.set_dot(10, 20).unwrap();
2461 grid.set_dot(30, 60).unwrap();
2462
2463 // Set colors
2464 grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2465
2466 // Resize
2467 grid.resize(30, 30).unwrap();
2468
2469 // Clear
2470 grid.clear();
2471
2472 // Verify final state
2473 assert_eq!(grid.dimensions(), (30, 30));
2474 assert!(grid.is_empty(0, 0));
2475
2476 // If this completes, logging worked throughout workflow
2477 }
2478
2479 // ========================================================================
2480 // Story 5.5: Apply Color Scheme to Intensity Buffer Tests (AC #3, #6)
2481 // ========================================================================
2482
2483 /// Test `apply_color_scheme()` basic functionality (AC #3)
2484 #[test]
2485 fn test_apply_color_scheme_basic() {
2486 use crate::color::schemes::grayscale;
2487
2488 let mut grid = BrailleGrid::new(4, 3).unwrap();
2489 let intensities: Vec<f32> = (0..12).map(|i| i as f32 / 11.0).collect();
2490 let scheme = grayscale();
2491
2492 let result = grid.apply_color_scheme(&intensities, &scheme);
2493 assert!(result.is_ok());
2494
2495 // First cell (intensity 0.0) -> black
2496 let c0 = grid.get_color(0, 0).unwrap();
2497 assert_eq!(c0, Color::black());
2498
2499 // Last cell (intensity 1.0) -> white
2500 let c_last = grid.get_color(3, 2).unwrap();
2501 assert_eq!(c_last, Color::white());
2502 }
2503
2504 /// Test `apply_color_scheme()` buffer size mismatch (AC #3)
2505 #[test]
2506 fn test_apply_color_scheme_buffer_mismatch() {
2507 use crate::color::schemes::grayscale;
2508
2509 let mut grid = BrailleGrid::new(4, 3).unwrap();
2510 let too_short: Vec<f32> = vec![0.5; 10]; // 10 < 12
2511 let too_long: Vec<f32> = vec![0.5; 15]; // 15 > 12
2512 let scheme = grayscale();
2513
2514 let result = grid.apply_color_scheme(&too_short, &scheme);
2515 assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
2516
2517 let result = grid.apply_color_scheme(&too_long, &scheme);
2518 assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
2519 }
2520
2521 /// Test `apply_color_scheme()` with special float values (AC #4)
2522 #[test]
2523 fn test_apply_color_scheme_special_floats() {
2524 use crate::color::schemes::grayscale;
2525
2526 let mut grid = BrailleGrid::new(5, 1).unwrap();
2527 let intensities = vec![
2528 f32::NAN,
2529 f32::INFINITY,
2530 f32::NEG_INFINITY,
2531 -0.5,
2532 1.5,
2533 ];
2534 let scheme = grayscale();
2535
2536 let result = grid.apply_color_scheme(&intensities, &scheme);
2537 assert!(result.is_ok());
2538
2539 // NaN -> 0.0 -> black
2540 assert_eq!(grid.get_color(0, 0), Some(Color::black()));
2541 // +Infinity -> 1.0 -> white
2542 assert_eq!(grid.get_color(1, 0), Some(Color::white()));
2543 // -Infinity -> 0.0 -> black
2544 assert_eq!(grid.get_color(2, 0), Some(Color::black()));
2545 // -0.5 clamped to 0.0 -> black
2546 assert_eq!(grid.get_color(3, 0), Some(Color::black()));
2547 // 1.5 clamped to 1.0 -> white
2548 assert_eq!(grid.get_color(4, 0), Some(Color::white()));
2549 }
2550
2551 /// Test `apply_color_scheme()` with heat_map scheme (AC #6, #7)
2552 #[test]
2553 fn test_apply_color_scheme_heat_map() {
2554 use crate::color::schemes::heat_map;
2555
2556 let mut grid = BrailleGrid::new(5, 1).unwrap();
2557 let intensities = vec![0.0, 0.25, 0.5, 0.75, 1.0];
2558 let scheme = heat_map();
2559
2560 grid.apply_color_scheme(&intensities, &scheme).unwrap();
2561
2562 // 0.0 -> black (cold)
2563 assert_eq!(grid.get_color(0, 0), Some(Color::black()));
2564 // 1.0 -> white (hot)
2565 assert_eq!(grid.get_color(4, 0), Some(Color::white()));
2566
2567 // Middle values should have color
2568 let mid = grid.get_color(2, 0).unwrap();
2569 assert!(mid.r > 0 || mid.g > 0 || mid.b > 0);
2570 }
2571
2572 /// Test `apply_color_scheme()` with rainbow scheme (AC #6, #7)
2573 #[test]
2574 fn test_apply_color_scheme_rainbow() {
2575 use crate::color::schemes::rainbow;
2576
2577 let mut grid = BrailleGrid::new(3, 1).unwrap();
2578 let intensities = vec![0.0, 0.5, 1.0];
2579 let scheme = rainbow();
2580
2581 grid.apply_color_scheme(&intensities, &scheme).unwrap();
2582
2583 // 0.0 -> red
2584 let red = grid.get_color(0, 0).unwrap();
2585 assert_eq!(red.r, 255);
2586 assert_eq!(red.g, 0);
2587 assert_eq!(red.b, 0);
2588
2589 // 1.0 -> purple-ish
2590 let purple = grid.get_color(2, 0).unwrap();
2591 assert!(purple.r > 200);
2592 assert!(purple.b > 200);
2593 }
2594
2595 /// Test `apply_color_scheme()` result is renderable (AC #5)
2596 #[test]
2597 fn test_apply_color_scheme_colors_renderable() {
2598 use crate::color::schemes::heat_map;
2599
2600 let mut grid = BrailleGrid::new(10, 10).unwrap();
2601
2602 // Set some dots
2603 grid.set_dot(5, 5).unwrap();
2604 grid.set_dot(10, 10).unwrap();
2605
2606 // Apply color scheme
2607 let intensities: Vec<f32> = (0..100)
2608 .map(|i| (i as f32) / 99.0)
2609 .collect();
2610 let scheme = heat_map();
2611 grid.apply_color_scheme(&intensities, &scheme).unwrap();
2612
2613 // All cells should have colors
2614 for y in 0..10 {
2615 for x in 0..10 {
2616 assert!(grid.get_color(x, y).is_some());
2617 }
2618 }
2619
2620 // Dots should still exist
2621 assert!(!grid.is_empty(2, 1)); // dot (5,5) -> cell (2,1)
2622 assert!(!grid.is_empty(5, 2)); // dot (10,10) -> cell (5,2)
2623 }
2624
2625 /// Test `apply_color_scheme()` empty grid (edge case)
2626 #[test]
2627 fn test_apply_color_scheme_single_cell() {
2628 use crate::color::schemes::grayscale;
2629
2630 let mut grid = BrailleGrid::new(1, 1).unwrap();
2631 let intensities = vec![0.5];
2632 let scheme = grayscale();
2633
2634 grid.apply_color_scheme(&intensities, &scheme).unwrap();
2635
2636 let color = grid.get_color(0, 0).unwrap();
2637 // 0.5 gray should be around 127-128
2638 assert!(color.r >= 127 && color.r <= 128);
2639 }
2640}