Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/graph/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,21 @@ impl Point {
return false;
}

let signs = vertices
let mut signs = vertices
.iter()
.zip(vertices.iter().cycle().skip(1))
.map(|(&a, &b)| {
let edge_vector = b - a;
let point_vector = *self - a;
edge_vector.cross(point_vector).signum()
})
.collect::<Vec<_>>();
});

let first_sign = signs[0];
let first_sign = signs.next().unwrap();
if first_sign == 0.0 {
return true;
}

signs.iter().all(|&s| s == 0.0 || s == first_sign)
signs.all(|s| s == 0.0 || s == first_sign)
}
}

Expand Down
198 changes: 122 additions & 76 deletions src/graph/image.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
borrow::Cow,
fmt::{self, Debug, Formatter},
hash::{Hash, Hasher},
io::Cursor,
Expand Down Expand Up @@ -694,6 +695,17 @@ pub fn calc_graph_row_image(
let image_width = (image_params.width as usize * cell_count) as u32;
let image_height = image_params.height as u32;

// Paint whole lanes from left to right, so higher lane indices cover lower ones.
let draw_order_key = |e: &Edge| (e.associated_line_pos_x, e.pos_x, e.edge_type);
let ordered_edges = if edges.is_sorted_by_key(draw_order_key) {
Cow::Borrowed(edges)
} else {
let mut ordered_edges = edges.to_vec();
ordered_edges.sort_unstable_by_key(draw_order_key);
Cow::Owned(ordered_edges)
};
let edges = ordered_edges.as_ref();

if image_params.antialias {
let image = antialias::render(
commit_pos_x,
Expand All @@ -714,35 +726,20 @@ pub fn calc_graph_row_image(
draw_background(&mut img_buf, image_params);
draw_commit_circle(&mut img_buf, commit_pos_x, image_params, drawing_pixels);

match graph_style {
GraphStyle::Curved => {
for edge in edges.iter().filter(|e| e.edge_type.is_vertically_related()) {
for lane_edges in edges.chunk_by(|a, b| a.associated_line_pos_x == b.associated_line_pos_x) {
for edge in lane_edges {
if graph_style == GraphStyle::Rounded || edge.edge_type.is_vertically_related() {
draw_edge(&mut img_buf, edge, image_params, drawing_pixels);
}
draw_curved_connected_edge(&mut img_buf, edges, image_params, drawing_pixels);
}
GraphStyle::Rounded => {
for edge in edges {
draw_edge(&mut img_buf, edge, image_params, drawing_pixels)
}
}
GraphStyle::Angular => {
let (vertial_edges, horizontal_edges): (Vec<&Edge>, Vec<&Edge>) = edges
.iter()
.partition(|e| e.edge_type.is_vertically_related());
for edge in vertial_edges {
draw_edge(&mut img_buf, edge, image_params, drawing_pixels)
}
let mut horizontal_edges_map: FxHashMap<usize, Vec<&Edge>> = FxHashMap::default();
for edge in horizontal_edges {
horizontal_edges_map
.entry(edge.associated_line_pos_x)
.or_default()
.push(edge);
}
for edges in horizontal_edges_map.values() {
draw_diagonal_connected_edge(&mut img_buf, edges, image_params, drawing_pixels);
}
if graph_style != GraphStyle::Rounded {
draw_connected_edges(
&mut img_buf,
lane_edges,
image_params,
drawing_pixels,
graph_style,
);
}
}

Expand Down Expand Up @@ -826,27 +823,39 @@ fn draw_edge(
}
}

fn draw_diagonal_connected_edge(
img_buf: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>,
edges: &[&Edge],
image_params: &ImageParams,
drawing_pixels: &mut DrawingPixels,
) {
for corner in edges
.iter()
.filter(|e| !e.edge_type.is_vertically_related())
{
fn edge_connections(edges: &[Edge]) -> impl Iterator<Item = (&Edge, &Edge)> {
edges.iter().filter_map(|corner| {
let side_type = match corner.edge_type {
EdgeType::RightBottom | EdgeType::RightTop => EdgeType::Right,
EdgeType::LeftBottom | EdgeType::LeftTop => EdgeType::Left,
_ => continue,
};
let Some(side) = edges.iter().find(|e| e.edge_type == side_type) else {
continue;
_ => return None,
};
let side = edges.iter().find(|e| {
e.edge_type == side_type && e.associated_line_pos_x == corner.associated_line_pos_x
})?;
Some((side, corner))
})
}

fn draw_connected_edges(
img_buf: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>,
edges: &[Edge],
image_params: &ImageParams,
drawing_pixels: &mut DrawingPixels,
graph_style: GraphStyle,
) {
for (side, corner) in edge_connections(edges) {
let color = image_params.edge_color(side.associated_line_pos_x);
let x_offset = (side.pos_x.min(corner.pos_x) * image_params.width as usize) as i32;
let pixels = drawing_pixels.diagonal_connection_pixels(side, corner, image_params);
let pixels = match graph_style {
GraphStyle::Angular => {
drawing_pixels.diagonal_connection_pixels(side, corner, image_params)
}
GraphStyle::Curved => {
drawing_pixels.curved_connection_pixels(side, corner, image_params)
}
GraphStyle::Rounded => unreachable!(),
};
draw_connection_pixels(img_buf, pixels, x_offset, color);
}
}
Expand Down Expand Up @@ -943,37 +952,6 @@ fn diagonal_connection(
)
}

fn draw_curved_connected_edge(
img_buf: &mut image::ImageBuffer<image::Rgba<u8>, Vec<u8>>,
edges: &[Edge],
image_params: &ImageParams,
drawing_pixels: &mut DrawingPixels,
) {
let corner_edges = edges.iter().filter(|e| {
matches!(
e.edge_type,
EdgeType::RightBottom | EdgeType::LeftBottom | EdgeType::RightTop | EdgeType::LeftTop
)
});

for corner in corner_edges {
let side_type = match corner.edge_type {
EdgeType::RightBottom | EdgeType::RightTop => EdgeType::Right,
_ => EdgeType::Left,
};
let Some(side) = edges.iter().find(|e| {
e.associated_line_pos_x == corner.associated_line_pos_x && e.edge_type == side_type
}) else {
continue;
};

let color = image_params.edge_color(corner.associated_line_pos_x);
let x_offset = (side.pos_x.min(corner.pos_x) * image_params.width as usize) as i32;
let pixels = drawing_pixels.curved_connection_pixels(side, corner, image_params);
draw_connection_pixels(img_buf, pixels, x_offset, color);
}
}

fn curved_connection_points(side: &Edge, corner: &Edge, image_params: &ImageParams) -> [Point; 4] {
let direction = if side.edge_type == EdgeType::Right {
1.0
Expand Down Expand Up @@ -1319,6 +1297,77 @@ mod tests {
);
}

#[rstest]
fn test_lane_paint_order(
#[values(GraphStyle::Rounded, GraphStyle::Angular, GraphStyle::Curved)] style: GraphStyle,
#[values(CellWidthType::Double, CellWidthType::Single)] cell_width_type: CellWidthType,
#[values(false, true)] antialias: bool,
) {
let colors = GraphColorSet::new(&GraphColorConfig::default());
let params = ImageParams::new(&colors, cell_width_type, antialias);
let mut pixels = DrawingPixels::new(&params);
let render = |edges: &[Edge], pixels: &mut DrawingPixels| {
calc_graph_row_image(5, 6, edges, &params, pixels, style)
};
let left_connection = vec![
Edge::new(LeftTop, 0, 0),
Edge::new(Horizontal, 1, 0),
Edge::new(Horizontal, 2, 0),
Edge::new(Horizontal, 3, 0),
Edge::new(Left, 4, 0),
];
let vertical = vec![Edge::new(Vertical, 1, 1)];
let right_connection = vec![
Edge::new(Right, 0, 4),
Edge::new(Horizontal, 1, 4),
Edge::new(Horizontal, 2, 4),
Edge::new(Horizontal, 3, 4),
Edge::new(RightTop, 4, 4),
];
let short_connection = vec![
Edge::new(Right, 0, 3),
Edge::new(Horizontal, 1, 3),
Edge::new(Horizontal, 2, 3),
Edge::new(RightTop, 3, 3),
];

for (lower, higher) in [
(left_connection, vertical.clone()),
(vertical, right_connection.clone()),
(short_connection, right_connection),
] {
let lower_image = image::load_from_memory(&render(&lower, &mut pixels).bytes)
.unwrap()
.to_rgba8();
let higher_image = image::load_from_memory(&render(&higher, &mut pixels).bytes)
.unwrap()
.to_rgba8();
let lower_color = params.edge_color(lower[0].associated_line_pos_x);
let higher_color = params.edge_color(higher[0].associated_line_pos_x);
let mut edges = lower;
edges.extend(higher);
let combined = render(&edges, &mut pixels);
let combined_image = image::load_from_memory(&combined.bytes).unwrap().to_rgba8();
let mut overlap_count = 0;
for (x, y, pixel) in lower_image.enumerate_pixels() {
if *pixel == lower_color && *higher_image.get_pixel(x, y) == higher_color {
assert_eq!(
*combined_image.get_pixel(x, y),
higher_color,
"higher lane must cover lower lane at ({x}, {y})"
);
overlap_count += 1;
}
}
assert!(overlap_count > 0, "test lanes must overlap");

edges.reverse();
assert_eq!(combined.bytes, render(&edges, &mut pixels).bytes);
edges.rotate_left(1);
assert_eq!(combined.bytes, render(&edges, &mut pixels).bytes);
}
}

#[rstest]
#[case(RightTop)]
#[case(RightBottom)]
Expand Down Expand Up @@ -1454,7 +1503,7 @@ mod tests {
let aa_params = ImageParams::new(&colors, cell_width_type, true);
let mut aa_pixels = DrawingPixels::new(&aa_params);
for (pos, edges) in simple_test_params() {
let mut edges: Vec<_> = edges
let edges: Vec<_> = edges
.into_iter()
.map(|(t, x, line)| Edge::new(t, x, line))
.collect();
Expand All @@ -1474,9 +1523,6 @@ mod tests {
);
}
}
edges.reverse();
let reordered = calc_graph_row_image(pos, 4, &edges, &aa_params, &mut aa_pixels, style);
assert_eq!(aa.bytes, reordered.bytes);
}
}

Expand Down
86 changes: 38 additions & 48 deletions src/graph/image/antialias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use rustc_hash::FxHashMap;
use crate::graph::{
geometry::Point,
image::{
bezier_point, curved_connection_points, diagonal_connection, ConnectionKey, DrawingPixels,
GraphStyle, ImageParams, Pixels,
bezier_point, curved_connection_points, diagonal_connection, edge_connections,
ConnectionKey, DrawingPixels, GraphStyle, ImageParams, Pixels,
},
Edge, EdgeType,
};
Expand Down Expand Up @@ -156,53 +156,43 @@ pub fn render(
if params.circle_edge_color[3] != 0 {
image.paint(&masks.circle_edge, offset, params.circle_edge_color);
}
let mut ordered_edges: Vec<_> = edges.iter().collect();
ordered_edges.sort_by_key(|e| (e.associated_line_pos_x, e.pos_x, e.edge_type));
for edge in &ordered_edges {
if style != GraphStyle::Rounded && !edge.edge_type.is_vertically_related() {
continue;
}
let offset = (edge.pos_x * params.width as usize) as i32;
let color = params.edge_color(edge.associated_line_pos_x);
match edge.edge_type {
EdgeType::RightTop => image.paint(&masks.corners[0], offset, color),
EdgeType::LeftTop => image.paint(&masks.corners[1], offset, color),
EdgeType::RightBottom => image.paint(&masks.corners[2], offset, color),
EdgeType::LeftBottom => image.paint(&masks.corners[3], offset, color),
t => image.paint_pixels(
match t {
EdgeType::Vertical => &pixels.vertical_edge,
EdgeType::Horizontal => &pixels.horizontal_edge,
EdgeType::Up => &pixels.up_edge,
EdgeType::Down => &pixels.down_edge,
EdgeType::Left => &pixels.left_edge,
EdgeType::Right => &pixels.right_edge,
_ => unreachable!(),
},
offset,
color,
),
}
}
if style != GraphStyle::Rounded {
for corner in ordered_edges {
let side_type = match corner.edge_type {
EdgeType::RightTop | EdgeType::RightBottom => EdgeType::Right,
EdgeType::LeftTop | EdgeType::LeftBottom => EdgeType::Left,
_ => continue,
};
let Some(side) = edges.iter().find(|e| {
e.edge_type == side_type && e.associated_line_pos_x == corner.associated_line_pos_x
}) else {
for lane_edges in edges.chunk_by(|a, b| a.associated_line_pos_x == b.associated_line_pos_x) {
for edge in lane_edges {
if style != GraphStyle::Rounded && !edge.edge_type.is_vertically_related() {
continue;
};
let offset = (side.pos_x.min(corner.pos_x) * params.width as usize) as i32;
let coverage = masks.connection_coverage(side, corner, params, style);
image.paint(
coverage,
offset,
params.edge_color(corner.associated_line_pos_x),
);
}
let offset = (edge.pos_x * params.width as usize) as i32;
let color = params.edge_color(edge.associated_line_pos_x);
match edge.edge_type {
EdgeType::RightTop => image.paint(&masks.corners[0], offset, color),
EdgeType::LeftTop => image.paint(&masks.corners[1], offset, color),
EdgeType::RightBottom => image.paint(&masks.corners[2], offset, color),
EdgeType::LeftBottom => image.paint(&masks.corners[3], offset, color),
t => image.paint_pixels(
match t {
EdgeType::Vertical => &pixels.vertical_edge,
EdgeType::Horizontal => &pixels.horizontal_edge,
EdgeType::Up => &pixels.up_edge,
EdgeType::Down => &pixels.down_edge,
EdgeType::Left => &pixels.left_edge,
EdgeType::Right => &pixels.right_edge,
_ => unreachable!(),
},
offset,
color,
),
}
}
if style != GraphStyle::Rounded {
for (side, corner) in edge_connections(lane_edges) {
let offset = (side.pos_x.min(corner.pos_x) * params.width as usize) as i32;
let coverage = masks.connection_coverage(side, corner, params, style);
image.paint(
coverage,
offset,
params.edge_color(corner.associated_line_pos_x),
);
}
}
}
image.resolve()
Expand Down
Binary file modified tests/graph/branch_001_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/branch_003_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/branch_004_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/branch_006_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_001_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_001_angular_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_001_curved.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_001_curved_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_angular_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_angular_single.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_angular_single_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_curved.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_curved_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_curved_single.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/complex_002_curved_single_antialias.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/image/color_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/merge_001_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/merge_002_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/merge_003_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/merge_004_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/merge_005_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/graph/stash_004_angular.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading