use crate::error::DotmaxError;
use crate::grid::{BrailleGrid, Color};
use crate::primitives::draw_line;
pub fn draw_circle(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
) -> Result<(), DotmaxError> {
if radius == 0 {
plot_dot_clipped(grid, center_x, center_y);
return Ok(());
}
#[allow(clippy::cast_possible_wrap)]
let mut x = radius as i32;
let mut y = 0i32;
let mut err = 1 - x;
while x >= y {
plot_8_symmetric_dots(grid, center_x, center_y, x, y);
y += 1;
if err < 0 {
err += 2 * y + 1;
} else {
x -= 1;
err += 2 * (y - x) + 1;
}
}
Ok(())
}
pub fn draw_circle_filled(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
) -> Result<(), DotmaxError> {
if radius == 0 {
plot_dot_clipped(grid, center_x, center_y);
return Ok(());
}
#[allow(clippy::cast_possible_wrap)]
let radius_i32 = radius as i32;
for dy in -radius_i32..=radius_i32 {
let y = center_y + dy;
#[allow(clippy::cast_precision_loss)]
let radius_f = radius as f32;
#[allow(clippy::cast_precision_loss)]
let dy_f = dy as f32;
#[allow(clippy::suboptimal_flops)]
let x_span = (radius_f * radius_f - dy_f * dy_f).sqrt();
#[allow(clippy::cast_possible_truncation)]
let x_offset = x_span.round() as i32;
let x_start = center_x - x_offset;
let x_end = center_x + x_offset;
draw_line(grid, x_start, y, x_end, y)?;
}
Ok(())
}
pub fn draw_circle_thick(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
thickness: u32,
) -> Result<(), DotmaxError> {
if thickness == 0 {
return Err(DotmaxError::InvalidThickness { thickness: 0 });
}
if thickness == 1 {
return draw_circle(grid, center_x, center_y, radius);
}
for i in 0..thickness {
draw_circle(grid, center_x, center_y, radius + i)?;
}
Ok(())
}
#[inline]
fn plot_8_symmetric_dots(grid: &mut BrailleGrid, center_x: i32, center_y: i32, x: i32, y: i32) {
plot_dot_clipped(grid, center_x + x, center_y + y);
plot_dot_clipped(grid, center_x + y, center_y + x);
plot_dot_clipped(grid, center_x - y, center_y + x);
plot_dot_clipped(grid, center_x - x, center_y + y);
plot_dot_clipped(grid, center_x - x, center_y - y);
plot_dot_clipped(grid, center_x - y, center_y - x);
plot_dot_clipped(grid, center_x + y, center_y - x);
plot_dot_clipped(grid, center_x + x, center_y - y);
}
#[inline]
fn plot_dot_clipped(grid: &mut BrailleGrid, x: i32, y: i32) {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_x = (grid.width() * 2) as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_y = (grid.height() * 4) as i32;
if x >= 0 && y >= 0 && x < max_x && y < max_y {
#[allow(clippy::cast_sign_loss)]
let _ = grid.set_dot(x as usize, y as usize);
}
}
pub fn draw_circle_colored(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
color: Color,
filled: bool,
) -> Result<(), DotmaxError> {
if filled {
draw_circle_filled_colored_impl(grid, center_x, center_y, radius, color)
} else {
draw_circle_outline_colored_impl(grid, center_x, center_y, radius, color)
}
}
#[allow(clippy::unnecessary_wraps)]
fn draw_circle_outline_colored_impl(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
color: Color,
) -> Result<(), DotmaxError> {
if radius == 0 {
plot_dot_colored_clipped(grid, center_x, center_y, color);
return Ok(());
}
#[allow(clippy::cast_possible_wrap)]
let mut x = radius as i32;
let mut y = 0i32;
let mut err = 1 - x;
while x >= y {
plot_8_symmetric_dots_colored(grid, center_x, center_y, x, y, color);
y += 1;
if err < 0 {
err += 2 * y + 1;
} else {
x -= 1;
err += 2 * (y - x) + 1;
}
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn draw_circle_filled_colored_impl(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
radius: u32,
color: Color,
) -> Result<(), DotmaxError> {
if radius == 0 {
plot_dot_colored_clipped(grid, center_x, center_y, color);
return Ok(());
}
#[allow(clippy::cast_possible_wrap)]
let mut x = radius as i32;
let mut y = 0i32;
let mut err = 1 - x;
while x >= y {
for scan_y in -y..=y {
plot_dot_colored_clipped(grid, center_x + x, center_y + scan_y, color);
plot_dot_colored_clipped(grid, center_x - x, center_y + scan_y, color);
}
if x != y {
for scan_y in -x..=x {
plot_dot_colored_clipped(grid, center_x + y, center_y + scan_y, color);
plot_dot_colored_clipped(grid, center_x - y, center_y + scan_y, color);
}
}
y += 1;
if err < 0 {
err += 2 * y + 1;
} else {
x -= 1;
err += 2 * (y - x) + 1;
}
}
Ok(())
}
#[inline]
fn plot_8_symmetric_dots_colored(
grid: &mut BrailleGrid,
center_x: i32,
center_y: i32,
x: i32,
y: i32,
color: Color,
) {
plot_dot_colored_clipped(grid, center_x + x, center_y + y, color);
plot_dot_colored_clipped(grid, center_x + y, center_y + x, color);
plot_dot_colored_clipped(grid, center_x - y, center_y + x, color);
plot_dot_colored_clipped(grid, center_x - x, center_y + y, color);
plot_dot_colored_clipped(grid, center_x - x, center_y - y, color);
plot_dot_colored_clipped(grid, center_x - y, center_y - x, color);
plot_dot_colored_clipped(grid, center_x + y, center_y - x, color);
plot_dot_colored_clipped(grid, center_x + x, center_y - y, color);
}
#[inline]
fn plot_dot_colored_clipped(grid: &mut BrailleGrid, x: i32, y: i32, color: Color) {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_x = (grid.width() * 2) as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let max_y = (grid.height() * 4) as i32;
if x >= 0 && y >= 0 && x < max_x && 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);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_dot_set(grid: &BrailleGrid, x: i32, y: i32) -> bool {
if x < 0 || y < 0 {
return false;
}
let x = x as usize;
let y = y as usize;
let max_x = grid.width() * 2;
let max_y = grid.height() * 4;
if x >= max_x || y >= max_y {
return false;
}
let cell_x = x / 2;
let cell_y = y / 4;
let dot_x = x % 2;
let dot_y = y % 4;
let braille_char = grid.get_char(cell_x, cell_y);
let dot_bit = match (dot_x, dot_y) {
(0, 0) => 0x01, (0, 1) => 0x02, (0, 2) => 0x04, (0, 3) => 0x40, (1, 0) => 0x08, (1, 1) => 0x10, (1, 2) => 0x20, (1, 3) => 0x80, _ => return false,
};
let braille_value = braille_char as u32 - 0x2800;
(braille_value & dot_bit) != 0
}
fn count_dots(grid: &BrailleGrid) -> usize {
let mut count = 0;
for y in 0..(grid.height() * 4) {
for x in 0..(grid.width() * 2) {
if is_dot_set(grid, x as i32, y as i32) {
count += 1;
}
}
}
count
}
#[test]
fn test_zero_radius() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
draw_circle(&mut grid, 10, 20, 0).unwrap();
assert!(is_dot_set(&grid, 10, 20));
assert_eq!(count_dots(&grid), 1);
}
#[test]
fn test_small_circle_symmetry() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
let center_x = 20;
let center_y = 40;
let radius = 5;
draw_circle(&mut grid, center_x, center_y, radius).unwrap();
let dot_count = count_dots(&grid);
assert!(dot_count > 0, "Circle should have dots");
assert!(dot_count < 100, "Small circle shouldn't have too many dots");
}
#[test]
fn test_boundary_clipping() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
draw_circle(&mut grid, -50, -50, 100).unwrap();
}
#[test]
fn test_extreme_coordinates() {
let mut grid = BrailleGrid::new(10, 10).unwrap();
draw_circle(&mut grid, -10000, -10000, 50000).unwrap();
}
#[test]
fn test_filled_circle_interior() {
let mut grid = BrailleGrid::new(50, 50).unwrap();
let center_x = 50;
let center_y = 100;
let radius = 20;
draw_circle_filled(&mut grid, center_x, center_y, radius).unwrap();
assert!(is_dot_set(&grid, center_x, center_y));
assert!(is_dot_set(&grid, center_x + 5, center_y));
assert!(is_dot_set(&grid, center_x - 5, center_y));
assert!(is_dot_set(&grid, center_x, center_y + 5));
assert!(is_dot_set(&grid, center_x, center_y - 5));
let filled_count = count_dots(&grid);
let mut grid_outline = BrailleGrid::new(50, 50).unwrap();
draw_circle(&mut grid_outline, center_x, center_y, radius).unwrap();
let outline_count = count_dots(&grid_outline);
assert!(
filled_count > outline_count * 5,
"Filled circle should have many more dots than outline"
);
}
#[test]
fn test_thick_circle_outline() {
let mut grid = BrailleGrid::new(50, 50).unwrap();
let center_x = 50;
let center_y = 100;
let radius = 25;
let thickness = 5;
draw_circle_thick(&mut grid, center_x, center_y, radius, thickness).unwrap();
let thick_count = count_dots(&grid);
let mut grid_thin = BrailleGrid::new(50, 50).unwrap();
draw_circle(&mut grid_thin, center_x, center_y, radius).unwrap();
let thin_count = count_dots(&grid_thin);
assert!(
thick_count > thin_count * 2,
"Thick circle should have significantly more dots than thin"
);
}
#[test]
fn test_invalid_thickness_zero() {
let mut grid = BrailleGrid::new(20, 20).unwrap();
let result = draw_circle_thick(&mut grid, 20, 40, 10, 0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DotmaxError::InvalidThickness { thickness: 0 }
));
}
#[test]
fn test_medium_circle_shape() {
let mut grid = BrailleGrid::new(50, 50).unwrap();
draw_circle(&mut grid, 50, 100, 25).unwrap();
let dot_count = count_dots(&grid);
assert!(
dot_count > 100 && dot_count < 300,
"Medium circle should have reasonable dot count"
);
}
#[test]
fn test_large_circle_correctness() {
let mut grid = BrailleGrid::new(100, 100).unwrap();
draw_circle(&mut grid, 100, 200, 50).unwrap();
let dot_count = count_dots(&grid);
assert!(dot_count > 200, "Large circle should have many dots");
}
}