use crate::error::DotmaxError;
use crate::grid::{BrailleGrid, Color};
use crate::primitives::line::{draw_line, draw_line_colored};
pub fn draw_rectangle(
grid: &mut BrailleGrid,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Result<(), DotmaxError> {
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidDimensions {
width: width as usize,
height: height as usize,
});
}
#[allow(clippy::cast_possible_wrap)]
let w = width as i32;
#[allow(clippy::cast_possible_wrap)]
let h = height as i32;
let x_right = x + w - 1;
let y_bottom = y + h - 1;
draw_line(grid, x, y, x_right, y)?; draw_line(grid, x_right, y, x_right, y_bottom)?; draw_line(grid, x_right, y_bottom, x, y_bottom)?; draw_line(grid, x, y_bottom, x, y)?;
Ok(())
}
pub fn draw_rectangle_filled(
grid: &mut BrailleGrid,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Result<(), DotmaxError> {
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidDimensions {
width: width as usize,
height: height as usize,
});
}
#[allow(clippy::cast_possible_wrap)]
let w = width as i32;
#[allow(clippy::cast_possible_wrap)]
let h = height as i32;
let x_right = x + w - 1;
for row in 0..h {
let current_y = y + row;
draw_line(grid, x, current_y, x_right, current_y)?;
}
Ok(())
}
pub fn draw_rectangle_thick(
grid: &mut BrailleGrid,
x: i32,
y: i32,
width: u32,
height: u32,
thickness: u32,
) -> Result<(), DotmaxError> {
if thickness == 0 {
return Err(DotmaxError::InvalidThickness { thickness: 0 });
}
if thickness > width / 2 || thickness > height / 2 {
return Err(DotmaxError::InvalidDimensions {
width: width as usize,
height: height as usize,
});
}
for i in 0..thickness {
#[allow(clippy::cast_possible_wrap)]
let offset = i as i32;
let new_x = x + offset;
let new_y = y + offset;
let new_width = width - (2 * i);
let new_height = height - (2 * i);
draw_rectangle(grid, new_x, new_y, new_width, new_height)?;
}
Ok(())
}
pub fn draw_polygon(grid: &mut BrailleGrid, vertices: &[(i32, i32)]) -> Result<(), DotmaxError> {
if vertices.len() < 3 {
return Err(DotmaxError::InvalidPolygon {
reason: format!("Polygon requires ≥3 vertices, got {}", vertices.len()),
});
}
for i in 0..vertices.len() - 1 {
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[i + 1];
draw_line(grid, x0, y0, x1, y1)?;
}
let (x_last, y_last) = vertices[vertices.len() - 1];
let (x_first, y_first) = vertices[0];
draw_line(grid, x_last, y_last, x_first, y_first)?;
Ok(())
}
pub fn draw_polygon_filled(
grid: &mut BrailleGrid,
vertices: &[(i32, i32)],
) -> Result<(), DotmaxError> {
#[derive(Debug)]
struct Edge {
y_min: i32,
y_max: i32,
x_at_y_min: f64,
inv_slope: f64, }
if vertices.len() < 3 {
return Err(DotmaxError::InvalidPolygon {
reason: format!("Polygon requires ≥3 vertices, got {}", vertices.len()),
});
}
let mut y_min = vertices[0].1;
let mut y_max = vertices[0].1;
for &(_, y) in vertices {
y_min = y_min.min(y);
y_max = y_max.max(y);
}
let mut edges = Vec::new();
for i in 0..vertices.len() {
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[(i + 1) % vertices.len()];
if y0 == y1 {
continue;
}
#[allow(clippy::cast_precision_loss)]
let (y_min_edge, y_max_edge, x_at_min, dx, dy) = if y0 < y1 {
(
y0,
y1,
f64::from(x0),
f64::from(x1 - x0),
f64::from(y1 - y0),
)
} else {
(
y1,
y0,
f64::from(x1),
f64::from(x0 - x1),
f64::from(y0 - y1),
)
};
edges.push(Edge {
y_min: y_min_edge,
y_max: y_max_edge,
x_at_y_min: x_at_min,
inv_slope: dx / dy,
});
}
for y in y_min..=y_max {
let mut intersections = Vec::new();
for edge in &edges {
if y >= edge.y_min && y < edge.y_max {
#[allow(clippy::cast_precision_loss)]
let offset = f64::from(y - edge.y_min);
#[allow(clippy::suboptimal_flops)]
let x_intersection = edge.x_at_y_min + edge.inv_slope * offset;
intersections.push(x_intersection);
}
}
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for pair in intersections.chunks(2) {
if pair.len() == 2 {
#[allow(clippy::cast_possible_truncation)]
let x_start = pair[0].round() as i32;
#[allow(clippy::cast_possible_truncation)]
let x_end = pair[1].round() as i32;
draw_line(grid, x_start, y, x_end, y)?;
}
}
}
Ok(())
}
pub fn draw_rectangle_colored(
grid: &mut BrailleGrid,
x: i32,
y: i32,
width: u32,
height: u32,
color: Color,
filled: bool,
) -> Result<(), DotmaxError> {
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidDimensions {
width: width as usize,
height: height as usize,
});
}
#[allow(clippy::cast_possible_wrap)]
let w = width as i32;
#[allow(clippy::cast_possible_wrap)]
let h = height as i32;
let x_right = x + w - 1;
if filled {
for row in 0..h {
let current_y = y + row;
draw_line_colored(grid, x, current_y, x_right, current_y, color, None)?;
}
} else {
let y_bottom = y + h - 1;
draw_line_colored(grid, x, y, x_right, y, color, None)?; draw_line_colored(grid, x_right, y, x_right, y_bottom, color, None)?; draw_line_colored(grid, x_right, y_bottom, x, y_bottom, color, None)?; draw_line_colored(grid, x, y_bottom, x, y, color, None)?; }
Ok(())
}
pub fn draw_polygon_colored(
grid: &mut BrailleGrid,
vertices: &[(i32, i32)],
color: Color,
closed: bool,
) -> Result<(), DotmaxError> {
if vertices.len() < 2 {
return Err(DotmaxError::InvalidPolygon {
reason: format!("Polygon requires ≥2 vertices, got {}", vertices.len()),
});
}
for i in 0..vertices.len() - 1 {
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[i + 1];
draw_line_colored(grid, x0, y0, x1, y1, color, None)?;
}
if closed && vertices.len() >= 2 {
let (x_last, y_last) = vertices[vertices.len() - 1];
let (x_first, y_first) = vertices[0];
draw_line_colored(grid, x_last, y_last, x_first, y_first, color, None)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_outline_small() {
let mut grid = BrailleGrid::new(40, 12).unwrap(); let result = draw_rectangle(&mut grid, 10, 10, 10, 10);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_outline_medium() {
let mut grid = BrailleGrid::new(40, 20).unwrap(); let result = draw_rectangle(&mut grid, 10, 10, 50, 25);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_outline_large() {
let mut grid = BrailleGrid::new(80, 24).unwrap(); let result = draw_rectangle(&mut grid, 5, 5, 100, 50);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_zero_width_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle(&mut grid, 10, 10, 0, 10);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_rectangle_zero_height_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle(&mut grid, 10, 10, 10, 0);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_rectangle_extreme_position_clipping() {
let mut grid = BrailleGrid::new(40, 12).unwrap(); let result = draw_rectangle(&mut grid, -10, -10, 50, 50);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_filled_small() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_filled(&mut grid, 10, 10, 10, 10);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_filled_medium() {
let mut grid = BrailleGrid::new(40, 20).unwrap();
let result = draw_rectangle_filled(&mut grid, 10, 10, 30, 20);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_thick_thickness_3() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_thick(&mut grid, 10, 10, 30, 20, 3);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_thick_corners_connected() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_thick(&mut grid, 10, 10, 40, 30, 5);
assert!(result.is_ok());
}
#[test]
fn test_rectangle_thick_zero_thickness_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_thick(&mut grid, 10, 10, 30, 20, 0);
assert!(matches!(result, Err(DotmaxError::InvalidThickness { .. })));
}
#[test]
fn test_rectangle_thick_exceeds_width_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_thick(&mut grid, 10, 10, 20, 30, 15);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_rectangle_thick_exceeds_height_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let result = draw_rectangle_thick(&mut grid, 10, 10, 30, 20, 15);
assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
}
#[test]
fn test_polygon_triangle() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (20, 40), (60, 40)];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_square() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(10, 10), (30, 10), (30, 30), (10, 30)];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_pentagon() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (50, 25), (45, 40), (35, 40), (30, 25)];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_hexagon() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (50, 20), (50, 35), (40, 45), (30, 35), (30, 20)];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_octagon() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [
(40, 10),
(50, 15),
(55, 25),
(55, 35),
(50, 45),
(40, 50),
(30, 45),
(25, 35),
];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_invalid_2_vertices_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(10, 10), (30, 30)];
let result = draw_polygon(&mut grid, &vertices);
assert!(matches!(result, Err(DotmaxError::InvalidPolygon { .. })));
}
#[test]
fn test_polygon_invalid_empty_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices: [(i32, i32); 0] = [];
let result = draw_polygon(&mut grid, &vertices);
assert!(matches!(result, Err(DotmaxError::InvalidPolygon { .. })));
}
#[test]
fn test_polygon_clipping_extreme_coords() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(-100, -100), (200, -50), (200, 200), (-100, 200)];
let result = draw_polygon(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_filled_triangle() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (20, 40), (60, 40)];
let result = draw_polygon_filled(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_filled_hexagon() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (50, 20), (50, 35), (40, 45), (30, 35), (30, 20)];
let result = draw_polygon_filled(&mut grid, &vertices);
assert!(result.is_ok());
}
#[test]
fn test_polygon_filled_invalid_vertices_error() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(10, 10), (30, 30)]; let result = draw_polygon_filled(&mut grid, &vertices);
assert!(matches!(result, Err(DotmaxError::InvalidPolygon { .. })));
}
#[test]
fn test_polygon_self_intersecting() {
let mut grid = BrailleGrid::new(40, 12).unwrap();
let vertices = [(40, 10), (45, 40), (10, 25), (70, 25), (35, 40)];
let result = draw_polygon_filled(&mut grid, &vertices);
assert!(result.is_ok());
}
}