Skip to main content

termplot_rs/
canvas.rs

1use colored::{Color, Colorize};
2use std::fmt::Write;
3
4pub struct BrailleCanvas {
5    pub width: usize,
6    pub height: usize,
7    // Buffer plano: width * height. Mucho más rápido que Vec<Vec<u8>>
8    buffer: Vec<u8>,
9    // Buffer plano de colores.
10    colors: Vec<Option<Color>>,
11    // Capa de texto plana
12    text_layer: Vec<Option<char>>,
13    // Buffer interno reutilizable (opcional, aquí creamos uno nuevo por frame para thread-safety simple)
14}
15
16impl BrailleCanvas {
17    pub fn new(width: usize, height: usize) -> Self {
18        let size = width * height;
19        Self {
20            width,
21            height,
22            buffer: vec![0u8; size],
23            colors: vec![None; size],
24            text_layer: vec![None; size],
25        }
26    }
27
28    /// Ancho en "sub-píxeles" (puntos braille)
29    #[inline]
30    pub fn pixel_width(&self) -> usize {
31        self.width * 2
32    }
33
34    /// Alto en "sub-píxeles" (puntos braille)
35    #[inline]
36    pub fn pixel_height(&self) -> usize {
37        self.height * 4
38    }
39
40    pub fn clear(&mut self) {
41        self.buffer.fill(0);
42        self.colors.fill(None);
43        self.text_layer.fill(None);
44    }
45
46    // --- Helpers de Coordenadas ---
47
48    #[inline]
49    fn idx(&self, col: usize, row: usize) -> usize {
50        row * self.width + col
51    }
52
53    fn set_pixel_impl(&mut self, px: usize, py: usize, color: Option<Color>) {
54        if px >= self.pixel_width() || py >= self.pixel_height() {
55            return;
56        }
57
58        let col = px / 2;
59        let row = py / 4;
60
61        let sub_x = px % 2;
62        let sub_y = py % 4;
63
64        let mask = match (sub_x, sub_y) {
65            (0, 0) => 0x01, (1, 0) => 0x08,
66            (0, 1) => 0x02, (1, 1) => 0x10,
67            (0, 2) => 0x04, (1, 2) => 0x20,
68            (0, 3) => 0x40, (1, 3) => 0x80,
69            _ => 0,
70        };
71
72        let index = self.idx(col, row);
73        self.buffer[index] |= mask;
74
75        if let Some(c) = color {
76            self.colors[index] = Some(c);
77        }
78    }
79
80    // --- API Pública de Dibujo ---
81
82    /// Coordenadas CARTESIANAS: (0,0) abajo-izquierda
83    pub fn set_pixel(&mut self, x: usize, y: usize, color: Option<Color>) {
84        let inverted_y = self.pixel_height().saturating_sub(1).saturating_sub(y);
85        self.set_pixel_impl(x, inverted_y, color);
86    }
87
88    /// Coordenadas DE PANTALLA: (0,0) arriba-izquierda
89    pub fn set_pixel_screen(&mut self, x: usize, y: usize, color: Option<Color>) {
90        self.set_pixel_impl(x, y, color);
91    }
92
93    /// Línea Cartesiana
94    pub fn line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
95        self.bresenham(x0, y0, x1, y1, color, true);
96    }
97
98    /// Línea de Pantalla
99    pub fn line_screen(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
100        self.bresenham(x0, y0, x1, y1, color, false);
101    }
102
103    fn bresenham(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>, cartesian: bool) {
104        let dx = (x1 - x0).abs();
105        let dy = -(y1 - y0).abs();
106        let sx = if x0 < x1 { 1 } else { -1 };
107        let sy = if y0 < y1 { 1 } else { -1 };
108        let mut err = dx + dy;
109        let mut x = x0;
110        let mut y = y0;
111
112        loop {
113            if x >= 0 && y >= 0 {
114                if cartesian {
115                    self.set_pixel(x as usize, y as usize, color);
116                } else {
117                    self.set_pixel_screen(x as usize, y as usize, color);
118                }
119            }
120            if x == x1 && y == y1 { break; }
121            let e2 = 2 * err;
122            if e2 >= dy { err += dy; x += sx; }
123            if e2 <= dx { err += dx; y += sy; }
124        }
125    }
126    
127    pub fn circle(&mut self, xc: isize, yc: isize, r: isize, color: Option<Color>) {
128        let mut x = 0;
129        let mut y = r;
130        let mut d = 3 - 2 * r;
131
132        let mut draw_octants = |cx: isize, cy: isize, x: isize, y: isize| {
133            let points = [
134                (cx + x, cy + y), (cx - x, cy + y), (cx + x, cy - y), (cx - x, cy - y),
135                (cx + y, cy + x), (cx - y, cy + x), (cx + y, cy - x), (cx - y, cy - x),
136            ];
137            for (px, py) in points {
138                if px >= 0 && py >= 0 {
139                    self.set_pixel(px as usize, py as usize, color);
140                }
141            }
142        };
143
144        draw_octants(xc, yc, x, y);
145        while y >= x {
146            x += 1;
147            if d > 0 {
148                y -= 1;
149                d = d + 4 * (x - y) + 10;
150            } else {
151                d = d + 4 * x + 6;
152            }
153            draw_octants(xc, yc, x, y);
154        }
155    }
156
157    pub fn set_char(&mut self, col: usize, row: usize, c: char, color: Option<Color>) {
158        // Mantenemos la lógica invertida para compatibilidad con charts actuales
159        let inverted_row = self.height.saturating_sub(1).saturating_sub(row);
160        
161        if col < self.width && inverted_row < self.height {
162            let idx = self.idx(col, inverted_row);
163            self.text_layer[idx] = Some(c);
164            if let Some(col_val) = color {
165                self.colors[idx] = Some(col_val);
166            }
167        }
168    }
169    
170    // --- Renderizado Optimizado ---
171
172    pub fn render(&self) -> String {
173        self.render_with_options(true, None)
174    }
175    
176    pub fn render_no_color(&self) -> String {
177        let mut output = String::with_capacity(self.width * self.height + self.height);
178        for row in 0..self.height {
179            for col in 0..self.width {
180                let idx = self.idx(col, row);
181                let mask = self.buffer[idx];
182                let ch = std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ');
183                output.push(ch);
184            }
185            output.push('\n');
186        }
187        output
188    }
189
190    pub fn render_with_options(&self, show_border: bool, title: Option<&str>) -> String {
191        let mut out = String::with_capacity(self.width * self.height * 15);
192
193        if let Some(t) = title {
194            let _ = writeln!(out, "{:^width$}", t, width = self.width + 2);
195        }
196
197        if show_border {
198            out.push('┌');
199            for _ in 0..self.width { out.push('─'); }
200            out.push('┐');
201            out.push('\n');
202        }
203
204        let mut last_color: Option<Color> = None;
205
206        for row in 0..self.height {
207            if show_border { out.push('│'); }
208
209            for col in 0..self.width {
210                let idx = self.idx(col, row);
211                
212                let char_to_print = if let Some(c) = self.text_layer[idx] {
213                    c
214                } else {
215                    let mask = self.buffer[idx];
216                    std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ')
217                };
218
219                let current_color = self.colors[idx];
220
221                // Optimización: Solo cambiar el código ANSI si el color es diferente al anterior
222                if current_color != last_color {
223                    match current_color {
224                        Some(c) => {
225                            // TRUCO: Creamos un string vacío coloreado "\x1b[31m\x1b[0m"
226                            // y reemplazamos el reset final para obtener solo el prefijo "\x1b[31m".
227                            // Esto funciona para cualquier Color (Standard o TrueColor).
228                            let ansi_full = format!("{}", "".color(c));
229                            let ansi_prefix = ansi_full.replace("\x1b[0m", "");
230                            out.push_str(&ansi_prefix);
231                        },
232                        None => {
233                             out.push_str("\x1b[0m"); 
234                        }
235                    }
236                    last_color = current_color;
237                }
238
239                out.push(char_to_print);
240            }
241            
242            // Reset al final de línea para seguridad
243            if last_color.is_some() {
244                out.push_str("\x1b[0m");
245                last_color = None;
246            }
247
248            if show_border { out.push('│'); }
249            out.push('\n');
250        }
251
252        if show_border {
253            out.push('└');
254            for _ in 0..self.width { out.push('─'); }
255            out.push('┘');
256        }
257
258        out
259    }
260}
261// --- TESTS UNITARIOS ---
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_coordinate_mapping() {
268        let canvas = BrailleCanvas::new(10, 5);
269        // La celda (0,0) es el índice 0
270        assert_eq!(canvas.idx(0, 0), 0);
271        // La celda (0,1) es el índice ancho (10)
272        assert_eq!(canvas.idx(0, 1), 10);
273    }
274
275    #[test]
276    fn test_braille_bitmask() {
277        // Creamos un canvas de 1x1 caracteres (2x4 píxeles)
278        let mut canvas = BrailleCanvas::new(1, 1);
279
280        // Píxel superior izquierda (0,0) -> Máscara 0x01
281        canvas.set_pixel_screen(0, 0, None);
282        assert_eq!(canvas.buffer[0], 0x01);
283
284        // Píxel a su derecha (1,0) -> Máscara 0x08
285        canvas.set_pixel_screen(1, 0, None);
286        assert_eq!(canvas.buffer[0], 0x01 | 0x08); // 0x09
287
288        // Píxel abajo del primero (0,1) -> Máscara 0x02
289        canvas.set_pixel_screen(0, 1, None);
290        assert_eq!(canvas.buffer[0], 0x09 | 0x02); // 0x0B
291    }
292
293    #[test]
294    fn test_clear() {
295        let mut canvas = BrailleCanvas::new(2, 2);
296        canvas.set_pixel(0, 0, None);
297        assert!(canvas.buffer.iter().any(|&x| x != 0));
298
299        canvas.clear();
300        assert!(canvas.buffer.iter().all(|&x| x == 0));
301        assert!(canvas.colors.iter().all(|x| x.is_none()));
302    }
303}