use rdom_tui::Color;
use rdom_tui::Style;
use rdom_tui::runtime::builtins::canvas::RenderContext;
use super::data::{ConnectPolicy, SeriesStyle};
use crate::palette::MUTED;
pub(crate) const BRAILLE_BASE: u32 = 0x2800;
pub(crate) fn dot_bit(sx: u8, sy: u8) -> u8 {
match (sx, sy) {
(0, 0) => 0,
(0, 1) => 1,
(0, 2) => 2,
(0, 3) => 6,
(1, 0) => 3,
(1, 1) => 4,
(1, 2) => 5,
(1, 3) => 7,
_ => 0,
}
}
pub(crate) struct BrailleGrid {
pub cell_width: u16,
pub cell_height: u16,
pub dots: Vec<u8>,
pub colors: Vec<Color>,
}
impl BrailleGrid {
pub fn new(cell_width: u16, cell_height: u16) -> Self {
let len = cell_width as usize * cell_height as usize;
Self {
cell_width,
cell_height,
dots: vec![0; len],
colors: vec![MUTED; len],
}
}
pub fn braille_width(&self) -> i32 {
self.cell_width as i32 * 2
}
pub fn braille_height(&self) -> i32 {
self.cell_height as i32 * 4
}
pub fn set_dot(&mut self, bx: i32, by: i32, color: Color) {
if bx < 0 || by < 0 || bx >= self.braille_width() || by >= self.braille_height() {
return;
}
let cx = (bx / 2) as usize;
let cy = (by / 4) as usize;
let sx = (bx % 2) as u8;
let sy = (by % 4) as u8;
let idx = cy * self.cell_width as usize + cx;
if idx < self.dots.len() {
self.dots[idx] |= 1 << dot_bit(sx, sy);
self.colors[idx] = color;
}
}
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
let mut x = x0;
let mut y = y0;
loop {
self.set_dot(x, y, color);
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
if x == x1 {
break;
}
err += dy;
x += sx;
}
if e2 <= dx {
if y == y1 {
break;
}
err += dx;
y += sy;
}
}
}
pub fn paint(&self, ctx: &mut RenderContext<'_>) {
for cy in 0..self.cell_height {
for cx in 0..self.cell_width {
let idx = cy as usize * self.cell_width as usize + cx as usize;
let bits = self.dots[idx];
if bits == 0 {
continue;
}
let ch = char::from_u32(BRAILLE_BASE | bits as u32).unwrap_or(' ');
ctx.set(cx, cy, ch, Style::new().fg(self.colors[idx]));
}
}
}
}
pub(crate) struct StackedPoint {
pub timestamp: f64,
pub top: f64,
}
pub(crate) struct ScaledPoint {
pub bx: i32,
pub top_by: i32,
}
pub(crate) struct ScaledSeries {
pub points: Vec<Option<ScaledPoint>>,
pub color: Color,
pub style: SeriesStyle,
pub connect: ConnectPolicy,
}
pub(crate) fn ema_smooth(points: &mut [StackedPoint], alpha: f64) {
if alpha <= 0.0 || points.len() < 2 {
return;
}
let mut ema = points[0].top;
for p in points.iter_mut() {
if p.top.is_finite() {
ema = alpha * p.top + (1.0 - alpha) * ema;
p.top = ema;
}
}
ema = points.last().unwrap().top;
for p in points.iter_mut().rev() {
if p.top.is_finite() {
ema = alpha * p.top + (1.0 - alpha) * ema;
p.top = ema;
}
}
}
pub(crate) fn render_series(grid: &mut BrailleGrid, series: &ScaledSeries) {
match series.style {
SeriesStyle::Line => render_line(grid, series),
}
}
fn render_line(grid: &mut BrailleGrid, series: &ScaledSeries) {
let mut prev: Option<&ScaledPoint> = None;
for pt in &series.points {
match pt {
Some(p) => {
grid.set_dot(p.bx, p.top_by, series.color);
if let Some(prev_p) = prev {
grid.draw_line(prev_p.bx, prev_p.top_by, p.bx, p.top_by, series.color);
}
prev = Some(p);
}
None => match series.connect {
ConnectPolicy::Gap => prev = None,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::palette::series_color;
#[test]
fn dot_bits_match_braille_layout() {
assert_eq!(dot_bit(0, 0), 0);
assert_eq!(dot_bit(0, 3), 6);
assert_eq!(dot_bit(1, 0), 3);
assert_eq!(dot_bit(1, 3), 7);
}
#[test]
fn all_dots_is_full_glyph() {
let all_bits: u8 = (0..4)
.flat_map(|sy| (0..2).map(move |sx| 1u8 << dot_bit(sx, sy)))
.fold(0u8, |acc, b| acc | b);
assert_eq!(all_bits, 0xFF);
assert_eq!(
char::from_u32(BRAILLE_BASE | all_bits as u32).unwrap(),
'\u{28FF}'
);
}
#[test]
fn set_dot_sets_correct_bit() {
let mut grid = BrailleGrid::new(2, 2);
assert_eq!(grid.braille_width(), 4);
assert_eq!(grid.braille_height(), 8);
grid.set_dot(0, 0, series_color(0));
assert_eq!(grid.dots[0], 1 << dot_bit(0, 0));
grid.set_dot(1, 0, series_color(1));
assert_eq!(grid.dots[0], (1 << dot_bit(0, 0)) | (1 << dot_bit(1, 0)));
assert_eq!(grid.colors[0], series_color(1));
}
#[test]
fn set_dot_out_of_bounds_is_noop() {
let mut grid = BrailleGrid::new(2, 2);
grid.set_dot(-1, 0, series_color(0));
grid.set_dot(0, -1, series_color(0));
grid.set_dot(100, 0, series_color(0));
grid.set_dot(0, 100, series_color(0));
assert!(grid.dots.iter().all(|&d| d == 0));
}
#[test]
fn horizontal_line_sets_row() {
let mut grid = BrailleGrid::new(4, 1);
grid.draw_line(0, 2, 7, 2, series_color(0));
for bx in 0..8 {
let cx = bx / 2;
let sx = (bx % 2) as u8;
assert!(
grid.dots[cx] & (1u8 << dot_bit(sx, 2)) != 0,
"missing dot at bx={bx}"
);
}
}
#[test]
fn diagonal_line_is_continuous() {
let mut grid = BrailleGrid::new(4, 2);
grid.draw_line(0, 0, 7, 7, series_color(0));
let total: u32 = grid.dots.iter().map(|&b| b.count_ones()).sum();
assert!(total >= 8, "expected continuous line, got {total} dots");
}
#[test]
fn ema_reduces_variance() {
let mut points: Vec<StackedPoint> = (0..10)
.map(|i| StackedPoint {
timestamp: i as f64,
top: if i % 2 == 0 { 100.0 } else { 0.0 },
})
.collect();
ema_smooth(&mut points, 0.3);
for p in &points {
assert!(p.top > 20.0 && p.top < 80.0, "value {} not near 50", p.top);
}
}
#[test]
fn ema_zero_alpha_passthrough() {
let mut points = vec![
StackedPoint {
timestamp: 0.0,
top: 10.0,
},
StackedPoint {
timestamp: 1.0,
top: 90.0,
},
];
ema_smooth(&mut points, 0.0);
assert_eq!(points[0].top, 10.0);
assert_eq!(points[1].top, 90.0);
}
}