use crate::error::DotmaxError;
use crate::grid::{BrailleGrid, Color};
pub fn draw_line(
grid: &mut BrailleGrid,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> Result<(), DotmaxError> {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_x = grid.dot_width() as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_y = grid.dot_height() as i32;
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 {
if x >= 0 && x < max_x && y >= 0 && y < max_y {
#[allow(clippy::cast_sign_loss)]
let _ = grid.set_dot(x as usize, y as usize);
}
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
Ok(())
}
pub fn draw_line_thick(
grid: &mut BrailleGrid,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
thickness: u32,
) -> Result<(), DotmaxError> {
if thickness == 0 {
return Err(DotmaxError::InvalidThickness { thickness: 0 });
}
if thickness == 1 {
return draw_line(grid, x0, y0, x1, y1);
}
let dx = x1 - x0;
let dy = y1 - y0;
let length = f64::from(dx * dx + dy * dy).sqrt();
if length == 0.0 {
#[allow(clippy::cast_possible_wrap)]
let half_thick = (thickness / 2) as i32;
for i in -(half_thick)..=(half_thick) {
for j in -(half_thick)..=(half_thick) {
draw_line(grid, x0 + i, y0 + j, x0 + i, y0 + j)?;
}
}
return Ok(());
}
#[allow(clippy::cast_possible_truncation)]
let perp_x = (f64::from(-dy) / length) as i32;
#[allow(clippy::cast_possible_truncation)]
let perp_y = (f64::from(dx) / length) as i32;
#[allow(clippy::cast_possible_wrap)]
let half_thickness = (thickness / 2) as i32;
for offset in -half_thickness..=half_thickness {
let offset_x = perp_x * offset;
let offset_y = perp_y * offset;
draw_line(
grid,
x0 + offset_x,
y0 + offset_y,
x1 + offset_x,
y1 + offset_y,
)?;
}
Ok(())
}
pub fn draw_line_colored(
grid: &mut BrailleGrid,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
color: Color,
thickness: Option<u32>,
) -> Result<(), DotmaxError> {
match thickness {
None | Some(1) => draw_line_colored_impl(grid, x0, y0, x1, y1, color),
Some(0) => Err(DotmaxError::InvalidThickness { thickness: 0 }),
Some(t) => draw_line_thick_colored_impl(grid, x0, y0, x1, y1, color, t),
}
}
#[allow(clippy::unnecessary_wraps)]
fn draw_line_colored_impl(
grid: &mut BrailleGrid,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
color: Color,
) -> Result<(), DotmaxError> {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_x = grid.dot_width() as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_y = grid.dot_height() as i32;
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 {
if x >= 0 && x < max_x && y >= 0 && y < max_y {
#[allow(clippy::cast_sign_loss)]
let dot_x = x as usize;
#[allow(clippy::cast_sign_loss)]
let dot_y = y as usize;
let _ = grid.set_dot(dot_x, dot_y);
let cell_x = dot_x / 2; let cell_y = dot_y / 4;
let _ = grid.set_cell_color(cell_x, cell_y, color);
}
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
Ok(())
}
fn draw_line_thick_colored_impl(
grid: &mut BrailleGrid,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
color: Color,
thickness: u32,
) -> Result<(), DotmaxError> {
let dx = x1 - x0;
let dy = y1 - y0;
let length = f64::from(dx * dx + dy * dy).sqrt();
if length == 0.0 {
#[allow(clippy::cast_possible_wrap)]
let half_thick = (thickness / 2) as i32;
for i in -(half_thick)..=(half_thick) {
for j in -(half_thick)..=(half_thick) {
draw_line_colored_impl(grid, x0 + i, y0 + j, x0 + i, y0 + j, color)?;
}
}
return Ok(());
}
#[allow(clippy::cast_possible_truncation)]
let perp_x = (f64::from(-dy) / length) as i32;
#[allow(clippy::cast_possible_truncation)]
let perp_y = (f64::from(dx) / length) as i32;
#[allow(clippy::cast_possible_wrap)]
let half_thickness = (thickness / 2) as i32;
for offset in -half_thickness..=half_thickness {
let offset_x = perp_x * offset;
let offset_y = perp_y * offset;
draw_line_colored_impl(
grid,
x0 + offset_x,
y0 + offset_y,
x1 + offset_x,
y1 + offset_y,
color,
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn is_dot_set(grid: &BrailleGrid, dot_x: usize, dot_y: usize) -> bool {
let cell_x = dot_x / 2;
let cell_y = dot_y / 4;
let local_x = dot_x % 2;
let local_y = dot_y % 4;
let dot_index = match (local_x, local_y) {
(0, 0) => 0, (0, 1) => 1, (0, 2) => 2, (0, 3) => 6, (1, 0) => 3, (1, 1) => 4, (1, 2) => 5, (1, 3) => 7, _ => return false,
};
grid.get_dot(cell_x, cell_y, dot_index).unwrap_or(false)
}
#[test]
fn test_horizontal_line() {
let mut grid = BrailleGrid::new(20, 10).unwrap();
draw_line(&mut grid, 0, 5, 10, 5).unwrap();
for x in 0..=10 {
assert!(is_dot_set(&grid, x, 5), "Dot at ({}, 5) should be set", x);
}
assert!(!is_dot_set(&grid, 0, 4), "Dot at (0, 4) should not be set");
assert!(!is_dot_set(&grid, 0, 6), "Dot at (0, 6) should not be set");
}
#[test]
fn test_vertical_line() {
let mut grid = BrailleGrid::new(20, 10).unwrap();
draw_line(&mut grid, 5, 0, 5, 10).unwrap();
for y in 0..=10 {
assert!(is_dot_set(&grid, 5, y), "Dot at (5, {}) should be set", y);
}
assert!(!is_dot_set(&grid, 4, 0), "Dot at (4, 0) should not be set");
assert!(!is_dot_set(&grid, 6, 0), "Dot at (6, 0) should not be set");
}
#[test]
fn test_diagonal_line_45deg() {
let mut grid = BrailleGrid::new(20, 10).unwrap();
draw_line(&mut grid, 0, 0, 10, 10).unwrap();
for i in 0..=10 {
assert!(
is_dot_set(&grid, i, i),
"Dot at ({}, {}) should be set",
i,
i
);
}
}
#[test]
fn test_arbitrary_angle() {
let mut grid = BrailleGrid::new(20, 10).unwrap();
draw_line(&mut grid, 0, 0, 10, 5).unwrap();
assert!(is_dot_set(&grid, 0, 0), "Start point should be set");
assert!(is_dot_set(&grid, 10, 5), "End point should be set");
let mut dots_set = 0;
for x in 0..=10 {
for y in 0..=5 {
if is_dot_set(&grid, x, y) {
dots_set += 1;
}
}
}
assert!(
dots_set >= 8,
"Line should set at least 8 dots, got {}",
dots_set
);
}
#[test]
fn test_boundary_clipping() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
draw_line(&mut grid, -10, -10, 10, 10).unwrap();
assert!(is_dot_set(&grid, 0, 0), "Origin should have dot");
assert!(is_dot_set(&grid, 10, 10), "Point (10,10) should have dot");
draw_line(&mut grid, -10000, -10000, 50000, 50000).unwrap();
}
#[test]
fn test_zero_length_line() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
draw_line(&mut grid, 5, 5, 5, 5).unwrap();
assert!(is_dot_set(&grid, 5, 5), "Single dot should be set");
}
#[test]
fn test_invalid_thickness() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
let result = draw_line_thick(&mut grid, 0, 0, 10, 10, 0);
assert!(result.is_err(), "Thickness=0 should return error");
match result {
Err(DotmaxError::InvalidThickness { thickness }) => {
assert_eq!(thickness, 0);
}
_ => panic!("Expected InvalidThickness error"),
}
}
#[test]
fn test_thick_line() {
let mut grid = BrailleGrid::new(20, 10).unwrap();
draw_line_thick(&mut grid, 5, 10, 5, 20, 1).unwrap();
let mut thin_dots = 0;
for x in 0..40 {
for y in 0..40 {
if is_dot_set(&grid, x, y) {
thin_dots += 1;
}
}
}
grid.clear();
draw_line_thick(&mut grid, 5, 10, 5, 20, 3).unwrap();
let mut thick_dots = 0;
for x in 0..40 {
for y in 0..40 {
if is_dot_set(&grid, x, y) {
thick_dots += 1;
}
}
}
assert!(
thick_dots > thin_dots,
"Thick line ({} dots) should have more dots than thin line ({} dots)",
thick_dots,
thin_dots
);
}
}