diff --git a/src/color.rs b/src/color.rs index b631869..d3d3192 100644 --- a/src/color.rs +++ b/src/color.rs @@ -7,11 +7,11 @@ pub struct Color { } impl Color { - fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } } - fn from_rgb(r: u8, g: u8, b: u8) -> Self { + pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { Self::from_rgba(r, g, b, 255) } diff --git a/src/graph/calc.rs b/src/graph/calc.rs index 6b5a958..f9faa88 100644 --- a/src/graph/calc.rs +++ b/src/graph/calc.rs @@ -20,7 +20,7 @@ pub struct Edge { } impl Edge { - fn new(edge_type: EdgeType, pos_x: usize, line_pos_x: usize) -> Self { + pub fn new(edge_type: EdgeType, pos_x: usize, line_pos_x: usize) -> Self { Self { edge_type, pos_x, diff --git a/src/graph/image.rs b/src/graph/image.rs index dbc9cd3..5be6e39 100644 --- a/src/graph/image.rs +++ b/src/graph/image.rs @@ -143,6 +143,14 @@ impl ImageParams { fn edge_color(&self, index: usize) -> image::Rgba { self.edge_colors[index % self.edge_colors.len()] } + + fn corner_radius(&self) -> u16 { + if self.width < self.height { + self.width / 2 + } else { + self.height / 2 + } + } } #[derive(Debug, Clone)] @@ -367,37 +375,80 @@ fn calc_right_edge_drawing_pixels(image_params: &ImageParams) -> Pixels { } fn calc_right_top_edge_drawing_pixels(image_params: &ImageParams) -> Pixels { - calc_corner_edge_drawing_pixels(image_params, 0, image_params.height) + let (w, h, r) = ( + image_params.width as i32, + image_params.height as i32, + image_params.corner_radius() as i32, + ); + let (x_offset, y_offset) = if w < h { + (0, r - (h / 2)) + } else { + ((w / 2) - r, 0) + }; + calc_corner_edge_drawing_pixels(image_params, 0, h, x_offset, y_offset) } fn calc_left_top_edge_drawing_pixels(image_params: &ImageParams) -> Pixels { - calc_corner_edge_drawing_pixels(image_params, image_params.width, image_params.height) + let (w, h, r) = ( + image_params.width as i32, + image_params.height as i32, + image_params.corner_radius() as i32, + ); + let (x_offset, y_offset) = if w < h { + (0, r - (h / 2)) + } else { + (r - (w / 2), 0) + }; + calc_corner_edge_drawing_pixels(image_params, w, h, x_offset, y_offset) } fn calc_right_bottom_edge_drawing_pixels(image_params: &ImageParams) -> Pixels { - calc_corner_edge_drawing_pixels(image_params, 0, 0) + let (w, h, r) = ( + image_params.width as i32, + image_params.height as i32, + image_params.corner_radius() as i32, + ); + let (x_offset, y_offset) = if w < h { + (0, (h / 2) - r) + } else { + ((w / 2) - r, 0) + }; + calc_corner_edge_drawing_pixels(image_params, 0, 0, x_offset, y_offset) } fn calc_left_bottom_edge_drawing_pixels(image_params: &ImageParams) -> Pixels { - calc_corner_edge_drawing_pixels(image_params, image_params.width, 0) + let (w, h, r) = ( + image_params.width as i32, + image_params.height as i32, + image_params.corner_radius() as i32, + ); + let (x_offset, y_offset) = if w < h { + (0, (h / 2) - r) + } else { + (r - (w / 2), 0) + }; + calc_corner_edge_drawing_pixels(image_params, w, 0, x_offset, y_offset) } fn calc_corner_edge_drawing_pixels( image_params: &ImageParams, - curve_center_x: u16, - curve_center_y: u16, + base_center_x: i32, + base_center_y: i32, + x_offset: i32, + y_offset: i32, ) -> Pixels { // Bresenham's circle algorithm - let curve_center_x = curve_center_x as i32; - let curve_center_y = curve_center_y as i32; + let curve_center_x = base_center_x; + let curve_center_y = base_center_y; let half_line_width = (image_params.line_width as i32) / 2; let adjust = if image_params.line_width % 2 == 0 { 0 } else { 1 }; - let inner_radius = (image_params.width / 2) as i32 - half_line_width - adjust; - let outer_radius = (image_params.width / 2) as i32 + half_line_width; + let radius_base_length = image_params.corner_radius() as i32; + let inner_radius = radius_base_length - half_line_width - adjust; + let outer_radius = radius_base_length + half_line_width; let mut x = inner_radius; let mut y = 0; @@ -449,7 +500,7 @@ fn calc_corner_edge_drawing_pixels( } } - outer_pixels + let mut pixels: Pixels = outer_pixels .difference(&inner_pixels) .filter(|p| { p.0 >= 0 @@ -457,8 +508,37 @@ fn calc_corner_edge_drawing_pixels( && p.1 >= 0 && p.1 < image_params.height as i32 }) - .cloned() - .collect() + .map(|p| (p.0 + x_offset, p.1 + y_offset)) + .collect(); + + if image_params.width < image_params.height { + let (ys, ye) = if y_offset < 0 { + (base_center_y + y_offset, base_center_y) + } else { + (base_center_y, base_center_y + y_offset) + }; + let center_x = (image_params.width / 2) as i32; + for x in (center_x - half_line_width)..=(center_x + half_line_width) { + for y in ys..ye { + pixels.insert((x, y)); + } + } + } + if image_params.width > image_params.height { + let (xs, xe) = if x_offset < 0 { + (base_center_x + x_offset, base_center_x) + } else { + (base_center_x, base_center_x + x_offset) + }; + let center_y = (image_params.height / 2) as i32; + for y in (center_y - half_line_width)..=(center_y + half_line_width) { + for x in xs..xe { + pixels.insert((x, y)); + } + } + } + + pixels } fn calc_graph_row_image( @@ -546,3 +626,226 @@ fn build_image(img_buf: &[u8], image_width: u32, image_height: u32) -> Vec { .unwrap(); bytes.into_inner() } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use image::GenericImage; + + use crate::color::Color; + + use super::*; + use EdgeType::*; + + const OUTPUT_DIR: &str = "./out/ut/graph/image"; + + type TestParam = (usize, Vec<(EdgeType, usize, usize)>); + + // Note: The output contents are not verified by the code. + + #[test] + fn test_calc_graph_row_image_default_params() { + let params = simple_test_params(); + let cell_count = 4; + let color_set = ColorSet::default(); + let image_params = ImageParams::new(&color_set); + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "default_params"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[test] + fn test_calc_graph_row_image_wide_image() { + let params = simple_test_params(); + let cell_count = 4; + let color_set = ColorSet::default(); + let mut image_params = ImageParams::new(&color_set); + image_params.width = 100; + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "wide_image"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[test] + fn test_calc_graph_row_image_tall_image() { + let params = simple_test_params(); + let cell_count = 4; + let color_set = ColorSet::default(); + let mut image_params = ImageParams::new(&color_set); + image_params.height = 100; + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "tall_image"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[test] + fn test_calc_graph_row_image_circle_radius() { + let params = straight_test_params(); + let cell_count = 2; + let color_set = ColorSet::default(); + let mut image_params = ImageParams::new(&color_set); + image_params.circle_inner_radius = 5; + image_params.circle_outer_radius = 12; + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "circle_radius"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[test] + fn test_calc_graph_row_image_line_width() { + let params = straight_test_params(); + let cell_count = 2; + let color_set = ColorSet::default(); + let mut image_params = ImageParams::new(&color_set); + image_params.line_width = 1; + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "line_width"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[test] + fn test_calc_graph_row_image_color() { + let params = branches_test_params(); + let cell_count = 7; + let color_set = ColorSet { + colors: vec![ + Color::from_rgb(200, 200, 100), + Color::from_rgb(100, 200, 200), + Color::from_rgb(100, 100, 100), + Color::from_rgb(200, 100, 200), + ], + }; + let image_params = ImageParams::new(&color_set); + let drawing_pixels = DrawingPixels::new(&image_params); + let file_name = "color"; + + test_calc_graph_row_image(params, cell_count, image_params, drawing_pixels, file_name); + } + + #[rustfmt::skip] + fn simple_test_params() -> Vec { + vec![ + (1, vec![(LeftBottom, 0, 0), (Left, 1, 0), (Down, 1, 1), (Right, 1, 3), (Horizontal, 2, 3), (RightBottom, 3, 3)]), + (3, vec![(Vertical, 0, 0), (Up, 3, 3), (Down, 3, 3)]), + (2, vec![(LeftTop, 0, 0), (Horizontal, 1, 0), (Left, 2, 0), (Up, 2, 2), (Right, 2, 3), (RightTop, 3, 3)]), + ] + } + + #[rustfmt::skip] + fn straight_test_params() -> Vec { + vec![ + (0, vec![(Up, 0, 0), (Down, 0, 0)]), + (0, vec![(Up, 0, 0), (Down, 0, 0), (Right, 0, 1), (RightBottom, 1, 1)]), + (1, vec![(Vertical, 0, 0), (Up, 1, 1), (Down, 1, 1)]), + (0, vec![(Up, 0, 0), (Down, 0, 0), (Right, 0, 1), (RightTop, 1, 1)]), + ] + } + + #[rustfmt::skip] + fn branches_test_params() -> Vec { + vec![ + (0, vec![(Up, 0, 0), (Down, 0, 0), + (Right, 0, 1), (RightBottom, 1, 1), + (Right, 0, 2), (Horizontal, 1, 2), (RightBottom, 2, 2), + (Right, 0, 3), (Horizontal, 1, 3), (Horizontal, 2, 3), (RightBottom, 3, 3), + (Right, 0, 4), (Horizontal, 1, 4), (Horizontal, 2, 4), (Horizontal, 3, 4), (RightBottom, 4, 4), + (Right, 0, 5), (Horizontal, 1, 5), (Horizontal, 2, 5), (Horizontal, 3, 5), (Horizontal, 4, 5), (RightBottom, 5, 5), + (Right, 0, 6), (Horizontal, 1, 6), (Horizontal, 2, 6), (Horizontal, 3, 6), (Horizontal, 4, 6), (Horizontal, 5, 6), (RightBottom, 6, 6)]), + (6, vec![(Vertical, 0, 0), (Vertical, 1, 1), (Vertical, 2, 2), (Vertical, 3, 3), (Vertical, 4, 4), (Vertical, 5, 5), (Down, 6, 6), (Up, 6, 6)]), + ] + } + + fn test_calc_graph_row_image( + params: Vec, + cell_count: usize, + image_params: ImageParams, + drawing_pixels: DrawingPixels, + file_name: &str, + ) { + let graph_row_images: Vec = params + .into_iter() + .map(|(commit_pos_x, edges)| { + let edges: Vec = edges + .into_iter() + .map(|t| Edge::new(t.0, t.1, t.2)) + .collect(); + calc_graph_row_image( + commit_pos_x, + cell_count, + &edges, + &image_params, + &drawing_pixels, + ) + }) + .collect(); + + save_image(&graph_row_images, &image_params, cell_count, file_name); + } + + fn save_image( + graph_row_images: &[GraphRowImage], + image_params: &ImageParams, + cell_count: usize, + file_name: &str, + ) { + let rows_len = graph_row_images.len() as u32; + let image_width = image_params.width as u32 * cell_count as u32; + let image_height = image_params.height as u32 * rows_len; + + let mut img_buf: image::ImageBuffer, Vec> = + image::ImageBuffer::new(image_width, image_height); + + for (i, graph_row_image) in graph_row_images.iter().enumerate() { + let image = image::load_from_memory(&graph_row_image.bytes).unwrap(); + let y = image_params.height as u32 * (rows_len - (i as u32) - 1); + img_buf.copy_from(&image, 0, y).unwrap(); + + for x in 0..cell_count { + let x_offset = x as u32 * image_params.width as u32; + let y_offset = y; + draw_border(&mut img_buf, image_params, x_offset, y_offset); + } + } + + create_output_dirs(OUTPUT_DIR); + let file_name = format!("{}/{}.png", OUTPUT_DIR, file_name); + image::save_buffer( + file_name, + &img_buf, + image_width, + image_height, + image::ColorType::Rgba8, + ) + .unwrap(); + } + + fn draw_border( + img_buf: &mut image::ImageBuffer, Vec>, + image_params: &ImageParams, + x_offset: u32, + y_offset: u32, + ) { + for x in 0..image_params.width { + for y in 0..image_params.height { + if x == 0 || x == image_params.width - 1 || y == 0 || y == image_params.height - 1 { + img_buf.put_pixel( + x as u32 + x_offset, + y as u32 + y_offset, + image::Rgba([255, 0, 0, 50]), + ); + } + } + } + } + + fn create_output_dirs(path: &str) { + let path = Path::new(path); + std::fs::create_dir_all(path).unwrap(); + } +}