Skip to content

Commit 1e03351

Browse files
committed
Support stroke outside/middle/inside to enable sharper strokes
1 parent 1f6ae49 commit 1e03351

File tree

2 files changed

+82
-18
lines changed

2 files changed

+82
-18
lines changed

crates/epaint/src/stroke.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ impl std::hash::Hash for Stroke {
5555
}
5656
}
5757

58+
#[derive(Clone, Debug, PartialEq, Eq)]
59+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
60+
pub enum StrokeKind {
61+
Outside,
62+
Inside,
63+
Middle,
64+
}
65+
66+
impl Default for StrokeKind {
67+
fn default() -> Self {
68+
Self::Outside
69+
}
70+
}
71+
5872
/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`]
5973
///
6074
/// The default stroke is the same as [`Stroke::NONE`].
@@ -63,20 +77,23 @@ impl std::hash::Hash for Stroke {
6377
pub struct PathStroke {
6478
pub width: f32,
6579
pub color: ColorMode,
80+
pub kind: StrokeKind,
6681
}
6782

6883
impl PathStroke {
6984
/// Same as [`PathStroke::default`].
7085
pub const NONE: Self = Self {
7186
width: 0.0,
7287
color: ColorMode::TRANSPARENT,
88+
kind: StrokeKind::Middle,
7389
};
7490

7591
#[inline]
7692
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
7793
Self {
7894
width: width.into(),
7995
color: ColorMode::Solid(color.into()),
96+
kind: StrokeKind::default(),
8097
}
8198
}
8299

@@ -91,6 +108,28 @@ impl PathStroke {
91108
Self {
92109
width: width.into(),
93110
color: ColorMode::UV(Arc::new(callback)),
111+
kind: StrokeKind::default(),
112+
}
113+
}
114+
115+
pub fn middle(self) -> Self {
116+
Self {
117+
kind: StrokeKind::Middle,
118+
..self
119+
}
120+
}
121+
122+
pub fn outside(self) -> Self {
123+
Self {
124+
kind: StrokeKind::Outside,
125+
..self
126+
}
127+
}
128+
129+
pub fn inside(self) -> Self {
130+
Self {
131+
kind: StrokeKind::Inside,
132+
..self
94133
}
95134
}
96135

@@ -116,6 +155,7 @@ impl From<Stroke> for PathStroke {
116155
Self {
117156
width: value.width,
118157
color: ColorMode::Solid(value.color),
158+
kind: StrokeKind::default(),
119159
}
120160
}
121161
}

crates/epaint/src/tessellator.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,6 @@ pub mod path {
532532
let r = clamp_rounding(rounding, rect);
533533

534534
if r == Rounding::ZERO {
535-
let min = rect.min;
536-
let max = rect.max;
537535
path.reserve(4);
538536
path.push(pos2(min.x, min.y)); // left top
539537
path.push(pos2(max.x, min.y)); // right top
@@ -868,6 +866,21 @@ fn fill_closed_path_with_uv(
868866
}
869867
}
870868

869+
/// Translate a point according to the stroke kind.
870+
fn translate_stroke_point(p: &PathPoint, stroke: &PathStroke) -> PathPoint {
871+
match stroke.kind {
872+
stroke::StrokeKind::Middle => p.clone(),
873+
stroke::StrokeKind::Outside => PathPoint {
874+
pos: p.pos + p.normal * stroke.width * 0.5,
875+
normal: p.normal,
876+
},
877+
stroke::StrokeKind::Inside => PathPoint {
878+
pos: p.pos - p.normal * stroke.width * 0.5,
879+
normal: p.normal,
880+
},
881+
}
882+
}
883+
871884
/// Tessellate the given path as a stroke with thickness.
872885
fn stroke_path(
873886
feathering: f32,
@@ -885,8 +898,13 @@ fn stroke_path(
885898
let idx = out.vertices.len() as u32;
886899

887900
// expand the bounding box to include the thickness of the path
888-
let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>())
889-
.expand((stroke.width / 2.0) + feathering);
901+
let bbox = Rect::from_points(
902+
&path
903+
.iter()
904+
.map(|p| translate_stroke_point(p, &stroke).pos)
905+
.collect::<Vec<Pos2>>(),
906+
)
907+
.expand((stroke.width / 2.0) + feathering);
890908

891909
let get_color = |col: &ColorMode, pos: Pos2| match col {
892910
ColorMode::Solid(col) => *col,
@@ -920,7 +938,7 @@ fn stroke_path(
920938
let mut i0 = n - 1;
921939
for i1 in 0..n {
922940
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
923-
let p1 = &path[i1 as usize];
941+
let p1 = translate_stroke_point(&path[i1 as usize], &stroke);
924942
let p = p1.pos;
925943
let n = p1.normal;
926944
out.colored_vertex(p + n * feathering, color_outer);
@@ -962,7 +980,7 @@ fn stroke_path(
962980

963981
let mut i0 = n - 1;
964982
for i1 in 0..n {
965-
let p1 = &path[i1 as usize];
983+
let p1 = translate_stroke_point(&path[i1 as usize], &stroke);
966984
let p = p1.pos;
967985
let n = p1.normal;
968986
out.colored_vertex(p + n * outer_rad, color_outer);
@@ -1007,7 +1025,7 @@ fn stroke_path(
10071025
out.reserve_vertices(4 * n as usize);
10081026

10091027
{
1010-
let end = &path[0];
1028+
let end = translate_stroke_point(&path[0], &stroke);
10111029
let p = end.pos;
10121030
let n = end.normal;
10131031
let back_extrude = n.rot90() * feathering;
@@ -1028,7 +1046,7 @@ fn stroke_path(
10281046

10291047
let mut i0 = 0;
10301048
for i1 in 1..n - 1 {
1031-
let point = &path[i1 as usize];
1049+
let point = translate_stroke_point(&path[i1 as usize], &stroke);
10321050
let p = point.pos;
10331051
let n = point.normal;
10341052
out.colored_vertex(p + n * outer_rad, color_outer);
@@ -1056,7 +1074,7 @@ fn stroke_path(
10561074

10571075
{
10581076
let i1 = n - 1;
1059-
let end = &path[i1 as usize];
1077+
let end = translate_stroke_point(&path[i1 as usize], &stroke);
10601078
let p = end.pos;
10611079
let n = end.normal;
10621080
let back_extrude = -n.rot90() * feathering;
@@ -1120,7 +1138,7 @@ fn stroke_path(
11201138
return;
11211139
}
11221140
}
1123-
for p in path {
1141+
for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) {
11241142
out.colored_vertex(
11251143
p.pos + radius * p.normal,
11261144
mul_color(
@@ -1138,7 +1156,7 @@ fn stroke_path(
11381156
}
11391157
} else {
11401158
let radius = stroke.width / 2.0;
1141-
for p in path {
1159+
for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) {
11421160
out.colored_vertex(
11431161
p.pos + radius * p.normal,
11441162
get_color(&stroke.color, p.pos + radius * p.normal),
@@ -1403,8 +1421,11 @@ impl Tessellator {
14031421
self.scratchpad_path.clear();
14041422
self.scratchpad_path.add_circle(center, radius);
14051423
self.scratchpad_path.fill(self.feathering, fill, out);
1406-
self.scratchpad_path
1407-
.stroke_closed(self.feathering, &stroke.into(), out);
1424+
self.scratchpad_path.stroke_closed(
1425+
self.feathering,
1426+
&PathStroke::from(stroke).outside(),
1427+
out,
1428+
);
14081429
}
14091430

14101431
/// Tessellate a single [`EllipseShape`] into a [`Mesh`].
@@ -1470,8 +1491,11 @@ impl Tessellator {
14701491
self.scratchpad_path.clear();
14711492
self.scratchpad_path.add_line_loop(&points);
14721493
self.scratchpad_path.fill(self.feathering, fill, out);
1473-
self.scratchpad_path
1474-
.stroke_closed(self.feathering, &stroke.into(), out);
1494+
self.scratchpad_path.stroke_closed(
1495+
self.feathering,
1496+
&PathStroke::from(stroke).outside(),
1497+
out,
1498+
);
14751499
}
14761500

14771501
/// Tessellate a single [`Mesh`] into a [`Mesh`].
@@ -1661,7 +1685,7 @@ impl Tessellator {
16611685
path.fill(self.feathering, fill, out);
16621686
}
16631687

1664-
path.stroke_closed(self.feathering, &stroke.into(), out);
1688+
path.stroke_closed(self.feathering, &PathStroke::from(stroke).outside(), out);
16651689
}
16661690

16671691
self.feathering = old_feathering; // restore
@@ -1700,8 +1724,8 @@ impl Tessellator {
17001724
// The contents of the galley is already snapped to pixel coordinates,
17011725
// but we need to make sure the galley ends up on the start of a physical pixel:
17021726
let galley_pos = pos2(
1703-
self.round_to_pixel(galley_pos.x),
1704-
self.round_to_pixel(galley_pos.y),
1727+
self.round_to_pixel(galley_pos.x) - 0.0,
1728+
self.round_to_pixel(galley_pos.y) - 0.0,
17051729
);
17061730

17071731
let uv_normalizer = vec2(

0 commit comments

Comments
 (0)