Skip to main content

termplot_rs/
canvas.rs

1use colored::Color;
2use std::fmt::{self, Write};
3
4#[derive(Clone, Copy, PartialEq, Eq)]
5pub enum ColorBlend {
6    /// Sobrescribe el color anterior de la celda.
7    Overwrite,
8    /// Mantiene el primer color que se asignó a la celda (no lo sobrescribe).
9    KeepFirst,
10}
11
12pub struct BrailleCanvas {
13    pub width: usize,
14    pub height: usize,
15    pub blend_mode: ColorBlend,
16    buffer: Vec<u8>,
17    colors: Vec<Option<Color>>,
18    text_layer: Vec<Option<char>>,
19}
20
21impl BrailleCanvas {
22    pub fn new(width: usize, height: usize) -> Self {
23        let size = width * height;
24        Self {
25            width,
26            height,
27            blend_mode: ColorBlend::Overwrite,
28            buffer: vec![0u8; size],
29            colors: vec![None; size],
30            text_layer: vec![None; size],
31        }
32    }
33
34    #[inline]
35    pub fn pixel_width(&self) -> usize {
36        self.width * 2
37    }
38
39    #[inline]
40    pub fn pixel_height(&self) -> usize {
41        self.height * 4
42    }
43
44    pub fn clear(&mut self) {
45        self.buffer.fill(0);
46        self.colors.fill(None);
47        self.text_layer.fill(None);
48    }
49
50    // --- Helpers de Coordenadas ---
51
52    #[inline]
53    fn idx(&self, col: usize, row: usize) -> usize {
54        row * self.width + col
55    }
56
57    #[inline]
58    fn get_mask(sub_x: usize, sub_y: usize) -> u8 {
59        match (sub_x, sub_y) {
60            (0, 0) => 0x01, (1, 0) => 0x08,
61            (0, 1) => 0x02, (1, 1) => 0x10,
62            (0, 2) => 0x04, (1, 2) => 0x20,
63            (0, 3) => 0x40, (1, 3) => 0x80,
64            _ => 0,
65        }
66    }
67
68    fn set_pixel_impl(&mut self, px: usize, py: usize, color: Option<Color>) {
69        if px >= self.pixel_width() || py >= self.pixel_height() {
70            return;
71        }
72
73        let index = self.idx(px / 2, py / 4);
74        self.buffer[index] |= Self::get_mask(px % 2, py % 4);
75
76        if let Some(c) = color {
77            match self.blend_mode {
78                ColorBlend::Overwrite => self.colors[index] = Some(c),
79                ColorBlend::KeepFirst => {
80                    if self.colors[index].is_none() {
81                        self.colors[index] = Some(c);
82                    }
83                }
84            }
85        }
86    }
87
88    fn unset_pixel_impl(&mut self, px: usize, py: usize) {
89        if px >= self.pixel_width() || py >= self.pixel_height() {
90            return;
91        }
92        let index = self.idx(px / 2, py / 4);
93        self.buffer[index] &= !Self::get_mask(px % 2, py % 4);
94        if self.buffer[index] == 0 {
95            self.colors[index] = None;
96        }
97    }
98
99    // --- API Pública de Dibujo Básica ---
100
101    pub fn set_pixel(&mut self, x: usize, y: usize, color: Option<Color>) {
102        let inverted_y = self.pixel_height().saturating_sub(1).saturating_sub(y);
103        self.set_pixel_impl(x, inverted_y, color);
104    }
105
106    pub fn set_pixel_screen(&mut self, x: usize, y: usize, color: Option<Color>) {
107        self.set_pixel_impl(x, y, color);
108    }
109
110    pub fn unset_pixel(&mut self, x: usize, y: usize) {
111        let inverted_y = self.pixel_height().saturating_sub(1).saturating_sub(y);
112        self.unset_pixel_impl(x, inverted_y);
113    }
114
115    pub fn unset_pixel_screen(&mut self, x: usize, y: usize) {
116        self.unset_pixel_impl(x, y);
117    }
118
119    pub fn toggle_pixel_screen(&mut self, x: usize, y: usize, color: Option<Color>) {
120        if x >= self.pixel_width() || y >= self.pixel_height() { return; }
121        let index = self.idx(x / 2, y / 4);
122        let mask = Self::get_mask(x % 2, y % 4);
123        
124        if (self.buffer[index] & mask) != 0 {
125            self.unset_pixel_impl(x, y);
126        } else {
127            self.set_pixel_impl(x, y, color);
128        }
129    }
130
131    // --- Primitivas con Clipping (Cohen-Sutherland) ---
132
133    fn compute_outcode(&self, x: isize, y: isize) -> u8 {
134        let mut code = 0;
135        let w = self.pixel_width() as isize;
136        let h = self.pixel_height() as isize;
137        
138        if x < 0 { code |= 1; } else if x >= w { code |= 2; }
139        if y < 0 { code |= 4; } else if y >= h { code |= 8; }
140        code
141    }
142
143    fn bresenham(&mut self, mut x0: isize, mut y0: isize, mut x1: isize, mut y1: isize, color: Option<Color>, cartesian: bool) {
144        let w = self.pixel_width() as isize;
145        let h = self.pixel_height() as isize;
146
147        // Cohen-Sutherland Clipping
148        let mut outcode0 = self.compute_outcode(x0, y0);
149        let mut outcode1 = self.compute_outcode(x1, y1);
150        let mut accept = false;
151
152        loop {
153            if (outcode0 | outcode1) == 0 {
154                accept = true; break;
155            } else if (outcode0 & outcode1) != 0 {
156                break;
157            } else {
158                let outcode_out = if outcode0 != 0 { outcode0 } else { outcode1 };
159                let mut x = 0;
160                let mut y = 0;
161
162                if outcode_out & 8 != 0 {
163                    x = x0 + (x1 - x0) * (h - 1 - y0) / (y1 - y0);
164                    y = h - 1;
165                } else if outcode_out & 4 != 0 {
166                    x = x0 + (x1 - x0) * (0 - y0) / (y1 - y0);
167                    y = 0;
168                } else if outcode_out & 2 != 0 {
169                    y = y0 + (y1 - y0) * (w - 1 - x0) / (x1 - x0);
170                    x = w - 1;
171                } else if outcode_out & 1 != 0 {
172                    y = y0 + (y1 - y0) * (0 - x0) / (x1 - x0);
173                    x = 0;
174                }
175
176                if outcode_out == outcode0 {
177                    x0 = x; y0 = y;
178                    outcode0 = self.compute_outcode(x0, y0);
179                } else {
180                    x1 = x; y1 = y;
181                    outcode1 = self.compute_outcode(x1, y1);
182                }
183            }
184        }
185
186        if !accept { return; }
187
188        let dx = (x1 - x0).abs();
189        let dy = -(y1 - y0).abs();
190        let sx = if x0 < x1 { 1 } else { -1 };
191        let sy = if y0 < y1 { 1 } else { -1 };
192        let mut err = dx + dy;
193
194        let mut x = x0;
195        let mut y = y0;
196
197        loop {
198            if cartesian {
199                self.set_pixel(x as usize, y as usize, color);
200            } else {
201                self.set_pixel_screen(x as usize, y as usize, color);
202            }
203            if x == x1 && y == y1 { break; }
204            let e2 = 2 * err;
205            if e2 >= dy { err += dy; x += sx; }
206            if e2 <= dx { err += dx; y += sy; }
207        }
208    }
209
210    pub fn line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
211        self.bresenham(x0, y0, x1, y1, color, true);
212    }
213
214    pub fn line_screen(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
215        self.bresenham(x0, y0, x1, y1, color, false);
216    }
217
218    // --- Primitivas 2D Completas ---
219
220    pub fn rect(&mut self, x: isize, y: isize, w: usize, h: usize, color: Option<Color>) {
221        let x1 = x + w as isize - 1;
222        let y1 = y + h as isize - 1;
223        self.line_screen(x, y, x1, y, color);
224        self.line_screen(x1, y, x1, y1, color);
225        self.line_screen(x1, y1, x, y1, color);
226        self.line_screen(x, y1, x, y, color);
227    }
228
229    pub fn rect_filled(&mut self, x: isize, y: isize, w: usize, h: usize, color: Option<Color>) {
230        let max_y = y + h as isize;
231        for cy in y..max_y {
232            self.line_screen(x, cy, x + w as isize - 1, cy, color);
233        }
234    }
235
236    pub fn circle(&mut self, xc: isize, yc: isize, r: isize, color: Option<Color>) {
237        let mut x = 0;
238        let mut y = r;
239        let mut d = 3 - 2 * r;
240
241        let mut draw_octants = |cx: isize, cy: isize, x: isize, y: isize| {
242            let points = [
243                (cx + x, cy + y), (cx - x, cy + y), (cx + x, cy - y), (cx - x, cy - y),
244                (cx + y, cy + x), (cx - y, cy + x), (cx + y, cy - x), (cx - y, cy - x),
245            ];
246            for (px, py) in points {
247                if px >= 0 && py >= 0 {
248                    self.set_pixel(px as usize, py as usize, color);
249                }
250            }
251        };
252
253        draw_octants(xc, yc, x, y);
254        while y >= x {
255            x += 1;
256            if d > 0 {
257                y -= 1;
258                d = d + 4 * (x - y) + 10;
259            } else {
260                d = d + 4 * x + 6;
261            }
262            draw_octants(xc, yc, x, y);
263        }
264    }
265
266    pub fn circle_filled(&mut self, xc: isize, yc: isize, r: isize, color: Option<Color>) {
267        let mut x = 0;
268        let mut y = r;
269        let mut d = 3 - 2 * r;
270
271        let mut draw_lines = |cx: isize, cy: isize, x: isize, y: isize| {
272            self.line(cx - x, cy + y, cx + x, cy + y, color);
273            self.line(cx - x, cy - y, cx + x, cy - y, color);
274            self.line(cx - y, cy + x, cx + y, cy + x, color);
275            self.line(cx - y, cy - x, cx + y, cy - x, color);
276        };
277
278        draw_lines(xc, yc, x, y);
279        while y >= x {
280            x += 1;
281            if d > 0 {
282                y -= 1;
283                d = d + 4 * (x - y) + 10;
284            } else {
285                d = d + 4 * x + 6;
286            }
287            draw_lines(xc, yc, x, y);
288        }
289    }
290
291    pub fn set_char(&mut self, col: usize, row: usize, c: char, color: Option<Color>) {
292        let inverted_row = self.height.saturating_sub(1).saturating_sub(row);
293        if col < self.width && inverted_row < self.height {
294            let idx = self.idx(col, inverted_row);
295            self.text_layer[idx] = Some(c);
296            if let Some(col_val) = color {
297                self.colors[idx] = Some(col_val);
298            }
299        }
300    }
301
302    // --- Renderizado Optimizado (Zero Allocation por frame posible) ---
303
304    /// Helper estático para evitar alocar Strings de `colored` en el formato estándar.
305    fn write_ansi_color<W: Write>(w: &mut W, color: Color) -> fmt::Result {
306        match color {
307            Color::Black => w.write_str("\x1b[30m"),
308            Color::Red => w.write_str("\x1b[31m"),
309            Color::Green => w.write_str("\x1b[32m"),
310            Color::Yellow => w.write_str("\x1b[33m"),
311            Color::Blue => w.write_str("\x1b[34m"),
312            Color::Magenta => w.write_str("\x1b[35m"),
313            Color::Cyan => w.write_str("\x1b[36m"),
314            Color::White => w.write_str("\x1b[37m"),
315            Color::BrightBlack => w.write_str("\x1b[90m"),
316            Color::BrightRed => w.write_str("\x1b[91m"),
317            Color::BrightGreen => w.write_str("\x1b[92m"),
318            Color::BrightYellow => w.write_str("\x1b[93m"),
319            Color::BrightBlue => w.write_str("\x1b[94m"),
320            Color::BrightMagenta => w.write_str("\x1b[95m"),
321            Color::BrightCyan => w.write_str("\x1b[96m"),
322            Color::BrightWhite => w.write_str("\x1b[97m"),
323            Color::TrueColor { r, g, b } => write!(w, "\x1b[38;2;{};{};{}m", r, g, b),
324        }
325    }
326
327    pub fn render_to<W: Write>(&self, w: &mut W, show_border: bool, title: Option<&str>) -> fmt::Result {
328        if let Some(t) = title {
329            writeln!(w, "{:^width$}", t, width = self.width + 2)?;
330        }
331
332        if show_border {
333            w.write_char('┌')?;
334            for _ in 0..self.width { w.write_char('─')?; }
335            w.write_char('┐')?;
336            w.write_char('\n')?;
337        }
338
339        let mut last_color: Option<Color> = None;
340
341        for row in 0..self.height {
342            if show_border { w.write_char('│')?; }
343
344            for col in 0..self.width {
345                let idx = self.idx(col, row);
346
347                let char_to_print = if let Some(c) = self.text_layer[idx] {
348                    c
349                } else {
350                    let mask = self.buffer[idx];
351                    std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ')
352                };
353
354                let current_color = self.colors[idx];
355
356                if current_color != last_color {
357                    match current_color {
358                        Some(c) => Self::write_ansi_color(w, c)?,
359                        None => w.write_str("\x1b[0m")?,
360                    }
361                    last_color = current_color;
362                }
363
364                w.write_char(char_to_print)?;
365            }
366
367            if last_color.is_some() {
368                w.write_str("\x1b[0m")?;
369                last_color = None;
370            }
371
372            if show_border { w.write_char('│')?; }
373            w.write_char('\n')?;
374        }
375
376        if show_border {
377            w.write_char('└')?;
378            for _ in 0..self.width { w.write_char('─')?; }
379            w.write_char('┘')?;
380        }
381
382        Ok(())
383    }
384
385    pub fn render_with_options(&self, show_border: bool, title: Option<&str>) -> String {
386        let mut out = String::with_capacity(self.width * self.height * 2 + 100);
387        let _ = self.render_to(&mut out, show_border, title);
388        out
389    }
390
391    pub fn render(&self) -> String {
392        self.render_with_options(true, None)
393    }
394
395    pub fn render_no_color(&self) -> String {
396        let mut out = String::with_capacity(self.width * self.height + self.height);
397        for row in 0..self.height {
398            for col in 0..self.width {
399                let mask = self.buffer[self.idx(col, row)];
400                out.push(std::char::from_u32(0x2800 + mask as u32).unwrap_or(' '));
401            }
402            out.push('\n');
403        }
404        out
405    }
406}