Skip to content

Commit 88a4b54

Browse files
authored
New graph style (#115)
* Add graph_style option and config * Implement angular style graph * Fix graph/image tests * Add integrated graph tests * Update README * Update config.schema.json * Extract geometric helper functions * Add comments
1 parent d982647 commit 88a4b54

28 files changed

+873
-77
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Options:
105105
-p, --protocol <TYPE> Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty]
106106
-o, --order <TYPE> Commit ordering algorithm [default: chrono] [possible values: chrono, topo]
107107
-g, --graph-width <TYPE> Commit graph image cell width [default: auto] [possible values: auto, double, single]
108+
-s, --graph-style <TYPE> Commit graph image edge style [default: rounded] [possible values: rounded, angular]
108109
-i, --initial-selection <TYPE> Initial selection of commit [default: latest] [possible values: latest, head]
109110
--preload Preload all graph images
110111
-h, --help Print help
@@ -155,6 +156,23 @@ If not specified or `auto` is specified, `double` will be used automatically if
155156

156157
</details>
157158

159+
#### -s, --graph-style \<TYPE\>
160+
161+
The commit graph image edge style.
162+
163+
`--graph-style rounded` will use rounded edges for the graph lines.
164+
165+
`--graph-style angular` will use angular edges for the graph lines.
166+
167+
<details>
168+
<summary>Screenshots</summary>
169+
170+
<img src="./img/style-angular.png" width=300>
171+
172+
`--graph-style angular`
173+
174+
</details>
175+
158176
#### -i, --initial-selection \<TYPE\>
159177

160178
The initial selection of commit when starting the application.

config.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141
],
4242
"default": "auto"
4343
},
44+
"graph_style": {
45+
"type": "string",
46+
"description": "The commit graph image edge style. The value specified in the command line argument takes precedence.",
47+
"enum": [
48+
"rounded",
49+
"angular"
50+
],
51+
"default": "rounded"
52+
},
4453
"initial_selection": {
4554
"type": "string",
4655
"description": "The initial selection of commit when starting the application. The value specified in the command line argument takes precedence.",

img/style-angular.png

132 KB
Loading

src/config.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use umbra::optional;
1111
use crate::{
1212
color::{ColorTheme, OptionalColorTheme},
1313
keybind::KeyBind,
14-
CommitOrderType, GraphWidthType, ImageProtocolType, InitialSelection, Result,
14+
CommitOrderType, GraphStyle, GraphWidthType, ImageProtocolType, InitialSelection, Result,
1515
};
1616

1717
const XDG_CONFIG_HOME_ENV_NAME: &str = "XDG_CONFIG_HOME";
@@ -111,6 +111,7 @@ pub struct CoreOptionConfig {
111111
pub protocol: Option<ImageProtocolType>,
112112
pub order: Option<CommitOrderType>,
113113
pub graph_width: Option<GraphWidthType>,
114+
pub graph_style: Option<GraphStyle>,
114115
pub initial_selection: Option<InitialSelection>,
115116
}
116117

@@ -332,6 +333,7 @@ mod tests {
332333
protocol: None,
333334
order: None,
334335
graph_width: None,
336+
graph_style: None,
335337
initial_selection: None,
336338
},
337339
search: CoreSearchConfig {
@@ -405,6 +407,7 @@ mod tests {
405407
protocol = "kitty"
406408
order = "topo"
407409
graph_width = "single"
410+
graph_style = "angular"
408411
initial_selection = "head"
409412
[core.search]
410413
ignore_case = true
@@ -442,6 +445,7 @@ mod tests {
442445
protocol: Some(ImageProtocolType::Kitty),
443446
order: Some(CommitOrderType::Topo),
444447
graph_width: Some(GraphWidthType::Single),
448+
graph_style: Some(GraphStyle::Angular),
445449
initial_selection: Some(InitialSelection::Head),
446450
},
447451
search: CoreSearchConfig {
@@ -528,6 +532,7 @@ mod tests {
528532
protocol: None,
529533
order: None,
530534
graph_width: None,
535+
graph_style: None,
531536
initial_selection: None,
532537
},
533538
search: CoreSearchConfig {

src/graph.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod calc;
2+
mod geometry;
23
mod image;
34

45
pub use calc::*;

src/graph/calc.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ pub enum EdgeType {
4343
LeftBottom, // ╰
4444
}
4545

46+
impl EdgeType {
47+
pub fn is_vertically_related(&self) -> bool {
48+
matches!(self, EdgeType::Vertical | EdgeType::Up | EdgeType::Down)
49+
}
50+
}
51+
4652
pub fn calc_graph(repository: &Repository) -> Graph<'_> {
4753
let commits = repository.all_commits();
4854

src/graph/geometry.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign};
2+
3+
#[derive(Debug, Clone, Copy, PartialEq)]
4+
pub struct Point {
5+
pub x: f64,
6+
pub y: f64,
7+
}
8+
9+
impl Point {
10+
pub fn new(x: f64, y: f64) -> Self {
11+
Self { x, y }
12+
}
13+
14+
pub fn is_inside_polygon(&self, vertices: &[Point]) -> bool {
15+
if vertices.len() < 3 {
16+
return false;
17+
}
18+
19+
let signs = vertices
20+
.iter()
21+
.zip(vertices.iter().cycle().skip(1))
22+
.map(|(&a, &b)| {
23+
let edge_vector = b - a;
24+
let point_vector = *self - a;
25+
edge_vector.cross(point_vector).signum()
26+
})
27+
.collect::<Vec<_>>();
28+
29+
let first_sign = signs[0];
30+
if first_sign == 0.0 {
31+
return true;
32+
}
33+
34+
signs.iter().all(|&s| s == 0.0 || s == first_sign)
35+
}
36+
}
37+
38+
impl Add<Vector> for Point {
39+
type Output = Point;
40+
41+
fn add(self, rhs: Vector) -> Self::Output {
42+
Point {
43+
x: self.x + rhs.x,
44+
y: self.y + rhs.y,
45+
}
46+
}
47+
}
48+
49+
impl Sub for Point {
50+
type Output = Vector;
51+
52+
fn sub(self, rhs: Point) -> Self::Output {
53+
Vector {
54+
x: self.x - rhs.x,
55+
y: self.y - rhs.y,
56+
}
57+
}
58+
}
59+
60+
impl Sub<Vector> for Point {
61+
type Output = Point;
62+
63+
fn sub(self, rhs: Vector) -> Self::Output {
64+
Point {
65+
x: self.x - rhs.x,
66+
y: self.y - rhs.y,
67+
}
68+
}
69+
}
70+
71+
#[derive(Debug, Clone, Copy, PartialEq)]
72+
pub struct Vector {
73+
pub x: f64,
74+
pub y: f64,
75+
}
76+
77+
impl Vector {
78+
pub fn new(x: f64, y: f64) -> Self {
79+
Self { x, y }
80+
}
81+
82+
pub fn length(&self) -> f64 {
83+
(self.x.powi(2) + self.y.powi(2)).sqrt()
84+
}
85+
86+
pub fn zero() -> Self {
87+
Self { x: 0.0, y: 0.0 }
88+
}
89+
90+
pub fn normalize(&self) -> Vector {
91+
let len = self.length();
92+
if len == 0.0 {
93+
Vector::zero()
94+
} else {
95+
*self / len
96+
}
97+
}
98+
99+
pub fn perpendicular(&self) -> Vector {
100+
Vector::new(-self.y, self.x)
101+
}
102+
103+
pub fn dot(&self, other: Vector) -> f64 {
104+
self.x * other.x + self.y * other.y
105+
}
106+
107+
pub fn cross(&self, other: Vector) -> f64 {
108+
self.x * other.y - self.y * other.x
109+
}
110+
}
111+
112+
impl Add for Vector {
113+
type Output = Vector;
114+
115+
fn add(self, rhs: Vector) -> Self::Output {
116+
Vector {
117+
x: self.x + rhs.x,
118+
y: self.y + rhs.y,
119+
}
120+
}
121+
}
122+
123+
impl AddAssign for Vector {
124+
fn add_assign(&mut self, rhs: Self) {
125+
self.x += rhs.x;
126+
self.y += rhs.y;
127+
}
128+
}
129+
130+
impl Sub for Vector {
131+
type Output = Vector;
132+
133+
fn sub(self, rhs: Vector) -> Self::Output {
134+
Vector {
135+
x: self.x - rhs.x,
136+
y: self.y - rhs.y,
137+
}
138+
}
139+
}
140+
141+
impl SubAssign for Vector {
142+
fn sub_assign(&mut self, rhs: Self) {
143+
self.x -= rhs.x;
144+
self.y -= rhs.y;
145+
}
146+
}
147+
148+
impl Mul<f64> for Vector {
149+
type Output = Vector;
150+
151+
fn mul(self, rhs: f64) -> Self::Output {
152+
Vector {
153+
x: self.x * rhs,
154+
y: self.y * rhs,
155+
}
156+
}
157+
}
158+
159+
impl Mul<Vector> for f64 {
160+
type Output = Vector;
161+
162+
fn mul(self, rhs: Vector) -> Self::Output {
163+
rhs * self
164+
}
165+
}
166+
167+
impl Div<f64> for Vector {
168+
type Output = Vector;
169+
170+
fn div(self, rhs: f64) -> Self::Output {
171+
Vector {
172+
x: self.x / rhs,
173+
y: self.y / rhs,
174+
}
175+
}
176+
}
177+
178+
pub fn bounding_box_u32(points: &[Point]) -> (u32, u32, u32, u32) {
179+
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
180+
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
181+
182+
for point in points {
183+
if point.x < min_x {
184+
min_x = point.x;
185+
}
186+
if point.y < min_y {
187+
min_y = point.y;
188+
}
189+
if point.x > max_x {
190+
max_x = point.x;
191+
}
192+
if point.y > max_y {
193+
max_y = point.y;
194+
}
195+
}
196+
197+
(min_x as u32, min_y as u32, max_x as u32, max_y as u32)
198+
}

0 commit comments

Comments
 (0)