Skip to content

Commit 48d42e8

Browse files
authored
feat: shape styling options and easier highlighting (#1210)
1 parent 233f14c commit 48d42e8

File tree

6 files changed

+594
-275
lines changed

6 files changed

+594
-275
lines changed

crates/rnote-compose/src/style/smooth/mod.rs

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
mod smoothoptions;
33

44
// Re-exports
5-
pub use smoothoptions::SmoothOptions;
5+
pub use smoothoptions::{LineCap, LineStyle, SmoothOptions};
66

77
// Imports
88
use super::Composer;
@@ -26,7 +26,12 @@ impl Composer<SmoothOptions> for Line {
2626

2727
if let Some(stroke_color) = options.stroke_color {
2828
let stroke_brush = cx.solid_brush(stroke_color.into());
29-
cx.stroke(line, &stroke_brush, options.stroke_width);
29+
cx.stroke_styled(
30+
line,
31+
&stroke_brush,
32+
options.stroke_width,
33+
&options.piet_stroke_style,
34+
);
3035
}
3136
cx.restore().unwrap();
3237
}
@@ -43,10 +48,11 @@ impl Composer<SmoothOptions> for Arrow {
4348

4449
if let Some(stroke_color) = options.stroke_color {
4550
let arrow = self.to_kurbo(Some(options.stroke_width));
46-
cx.stroke(
51+
cx.stroke_styled(
4752
arrow,
4853
&Into::<piet::Color>::into(stroke_color),
4954
options.stroke_width,
55+
&options.piet_stroke_style,
5056
);
5157
}
5258

@@ -70,7 +76,12 @@ impl Composer<SmoothOptions> for Rectangle {
7076

7177
if let Some(stroke_color) = options.stroke_color {
7278
let stroke_brush = cx.solid_brush(stroke_color.into());
73-
cx.stroke(shape, &stroke_brush, options.stroke_width);
79+
cx.stroke_styled(
80+
shape,
81+
&stroke_brush,
82+
options.stroke_width,
83+
&options.piet_stroke_style,
84+
);
7485
}
7586
cx.restore().unwrap();
7687
}
@@ -92,7 +103,12 @@ impl Composer<SmoothOptions> for Ellipse {
92103

93104
if let Some(stroke_color) = options.stroke_color {
94105
let stroke_brush = cx.solid_brush(stroke_color.into());
95-
cx.stroke(ellipse, &stroke_brush, options.stroke_width);
106+
cx.stroke_styled(
107+
ellipse,
108+
&stroke_brush,
109+
options.stroke_width,
110+
&options.piet_stroke_style,
111+
);
96112
}
97113
cx.restore().unwrap();
98114
}
@@ -114,7 +130,12 @@ impl Composer<SmoothOptions> for QuadraticBezier {
114130

115131
if let Some(stroke_color) = options.stroke_color {
116132
let stroke_brush = cx.solid_brush(stroke_color.into());
117-
cx.stroke(quadbez, &stroke_brush, options.stroke_width);
133+
cx.stroke_styled(
134+
quadbez,
135+
&stroke_brush,
136+
options.stroke_width,
137+
&options.piet_stroke_style,
138+
);
118139
}
119140
cx.restore().unwrap();
120141
}
@@ -136,7 +157,12 @@ impl Composer<SmoothOptions> for CubicBezier {
136157

137158
if let Some(stroke_color) = options.stroke_color {
138159
let stroke_brush = cx.solid_brush(stroke_color.into());
139-
cx.stroke(cubbez, &stroke_brush, options.stroke_width);
160+
cx.stroke_styled(
161+
cubbez,
162+
&stroke_brush,
163+
options.stroke_width,
164+
&options.piet_stroke_style,
165+
);
140166
}
141167
cx.restore().unwrap();
142168
}
@@ -161,13 +187,16 @@ impl Composer<SmoothOptions> for Polyline {
161187
&Into::<piet::Color>::into(color),
162188
);
163189
} else {
190+
let style = options
191+
.piet_stroke_style
192+
.clone()
193+
.line_cap(piet::LineCap::Butt)
194+
.line_join(piet::LineJoin::Bevel);
164195
cx.stroke_styled(
165196
self.outline_path(),
166197
&Into::<piet::Color>::into(color),
167198
options.stroke_width,
168-
&piet::StrokeStyle::default()
169-
.line_cap(piet::LineCap::Butt)
170-
.line_join(piet::LineJoin::Bevel),
199+
&style,
171200
);
172201
}
173202
}
@@ -196,14 +225,17 @@ impl Composer<SmoothOptions> for Polygon {
196225
if let Some(fill_color) = options.fill_color {
197226
cx.fill(&outline_path, &Into::<piet::Color>::into(fill_color));
198227
}
228+
let style = options
229+
.piet_stroke_style
230+
.clone()
231+
.line_cap(piet::LineCap::Butt)
232+
.line_join(piet::LineJoin::Bevel);
199233

200234
cx.stroke_styled(
201235
&outline_path,
202236
&Into::<piet::Color>::into(color),
203237
options.stroke_width,
204-
&piet::StrokeStyle::default()
205-
.line_cap(piet::LineCap::Butt)
206-
.line_join(piet::LineJoin::Bevel),
238+
&style,
207239
);
208240
}
209241
}

crates/rnote-compose/src/style/smooth/smoothoptions.rs

Lines changed: 230 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Imports
22
use crate::Color;
33
use crate::style::PressureCurve;
4+
use anyhow::Context;
5+
use num_derive::{FromPrimitive, ToPrimitive};
46
use serde::{Deserialize, Serialize};
7+
use std::{
8+
f64,
9+
ops::{AddAssign, MulAssign},
10+
};
511

6-
/// Options for shapes that can be drawn in a smooth style.
7-
#[derive(Debug, Clone, Serialize, Deserialize)]
8-
#[serde(default, rename = "smooth_options")]
12+
/// Options for shapes that can be drawn in a smooth style. Ensure the precursor struct used in deserialization matches this one.
13+
#[derive(Debug, Clone, Serialize)]
14+
#[serde(rename = "smooth_options")]
915
pub struct SmoothOptions {
1016
/// Stroke width.
1117
#[serde(rename = "stroke_width", with = "crate::serialize::f64_dp3")]
@@ -19,15 +25,235 @@ pub struct SmoothOptions {
1925
/// Pressure curve.
2026
#[serde(rename = "pressure_curve")]
2127
pub pressure_curve: PressureCurve,
28+
/// Line style.
29+
#[serde(rename = "line_style")]
30+
pub line_style: LineStyle,
31+
/// Line cap.
32+
#[serde(rename = "line_cap")]
33+
pub line_cap: LineCap,
34+
/// The inner piet::StrokeStyle, computed using the stroke_width, line_style, and line_cap.
35+
#[serde(skip)]
36+
pub piet_stroke_style: piet::StrokeStyle,
2237
}
2338

2439
impl Default for SmoothOptions {
2540
fn default() -> Self {
41+
let stroke_width: f64 = 2.0;
42+
let line_style = LineStyle::default();
43+
let line_cap = LineCap::default();
2644
Self {
27-
stroke_width: 2.0,
45+
stroke_width,
2846
stroke_color: Some(Color::BLACK),
2947
fill_color: None,
3048
pressure_curve: PressureCurve::default(),
49+
line_style,
50+
line_cap,
51+
piet_stroke_style: Self::compute_piet_stroke_style(stroke_width, line_style, line_cap),
3152
}
3253
}
3354
}
55+
56+
impl SmoothOptions {
57+
/// The ratio between the length of a dash and the width of the stroke
58+
const DASH_LENGTH_TO_WIDTH_RATIO: f64 = f64::consts::E;
59+
60+
fn compute_piet_stroke_style(
61+
stroke_width: f64,
62+
line_style: LineStyle,
63+
line_cap: LineCap,
64+
) -> piet::StrokeStyle {
65+
let mut dash_pattern = line_style.as_unscaled_vector();
66+
match line_cap {
67+
LineCap::Straight => dash_pattern
68+
.iter_mut()
69+
.for_each(|e| e.mul_assign(stroke_width * Self::DASH_LENGTH_TO_WIDTH_RATIO)),
70+
LineCap::Rounded => dash_pattern.iter_mut().enumerate().for_each(|(idx, e)| {
71+
if !line_style.is_dotted() {
72+
e.mul_assign(stroke_width * Self::DASH_LENGTH_TO_WIDTH_RATIO);
73+
}
74+
// If the stroke has a rounded linecap, a half-disk with radius equal to the stroke width is added both ends of a stroke, this increases the length of each line by the width of the stroke, and is not taken into account by DashStroke, it has to be manually accounted for
75+
if idx % 2 == 1 {
76+
e.add_assign(2.0 * stroke_width)
77+
}
78+
}),
79+
};
80+
let mut stroke_style = piet::StrokeStyle::new();
81+
stroke_style.set_dash_pattern(dash_pattern);
82+
stroke_style.set_line_cap(line_cap.into());
83+
stroke_style
84+
}
85+
86+
/// Updates the inner piet::Strokestyle
87+
pub fn update_piet_stroke_style(&mut self) {
88+
self.piet_stroke_style =
89+
Self::compute_piet_stroke_style(self.stroke_width, self.line_style, self.line_cap);
90+
}
91+
92+
/// Updates the line cap
93+
pub fn update_line_cap(&mut self, line_cap: LineCap) {
94+
// Dotted style requires a round LineCap
95+
if self.line_style.is_dotted() && line_cap != LineCap::Rounded {
96+
self.line_style = LineStyle::Solid;
97+
}
98+
self.line_cap = line_cap;
99+
self.update_piet_stroke_style();
100+
}
101+
102+
/// Updates the line style
103+
pub fn update_line_style(&mut self, line_style: LineStyle) {
104+
// Dotted style requires a round LineCap
105+
if line_style.is_dotted() {
106+
self.line_cap = LineCap::Rounded;
107+
}
108+
self.line_style = line_style;
109+
self.update_piet_stroke_style();
110+
}
111+
}
112+
113+
impl<'de> Deserialize<'de> for SmoothOptions {
114+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
115+
where
116+
D: serde::Deserializer<'de>,
117+
{
118+
#[derive(Deserialize)]
119+
#[serde(default, rename = "smooth_options")]
120+
struct SmoothOptionsPrecursor {
121+
#[serde(rename = "stroke_width", with = "crate::serialize::f64_dp3")]
122+
pub stroke_width: f64,
123+
#[serde(rename = "stroke_color")]
124+
pub stroke_color: Option<Color>,
125+
#[serde(rename = "fill_color")]
126+
pub fill_color: Option<Color>,
127+
#[serde(rename = "pressure_curve")]
128+
pub pressure_curve: PressureCurve,
129+
#[serde(rename = "line_style")]
130+
pub line_style: LineStyle,
131+
#[serde(rename = "line_cap")]
132+
pub line_cap: LineCap,
133+
}
134+
135+
impl From<SmoothOptions> for SmoothOptionsPrecursor {
136+
fn from(value: SmoothOptions) -> Self {
137+
Self {
138+
stroke_width: value.stroke_width,
139+
stroke_color: value.stroke_color,
140+
fill_color: value.fill_color,
141+
pressure_curve: value.pressure_curve,
142+
line_style: value.line_style,
143+
line_cap: value.line_cap,
144+
}
145+
}
146+
}
147+
148+
impl Default for SmoothOptionsPrecursor {
149+
fn default() -> Self {
150+
SmoothOptions::default().into()
151+
}
152+
}
153+
154+
let precursor = SmoothOptionsPrecursor::deserialize(deserializer)?;
155+
156+
Ok(SmoothOptions {
157+
stroke_width: precursor.stroke_width,
158+
stroke_color: precursor.stroke_color,
159+
fill_color: precursor.fill_color,
160+
pressure_curve: precursor.pressure_curve,
161+
line_style: precursor.line_style,
162+
line_cap: precursor.line_cap,
163+
piet_stroke_style: Self::compute_piet_stroke_style(
164+
precursor.stroke_width,
165+
precursor.line_style,
166+
precursor.line_cap,
167+
),
168+
})
169+
}
170+
}
171+
172+
/// Line cap present at the start and end of a line
173+
#[derive(
174+
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, FromPrimitive, ToPrimitive,
175+
)]
176+
#[serde(rename = "line_cap")]
177+
pub enum LineCap {
178+
/// Straight line cap
179+
#[default]
180+
#[serde(rename = "straight")]
181+
Straight,
182+
/// Rounded line cap
183+
#[serde(rename = "rounded")]
184+
Rounded,
185+
}
186+
187+
impl TryFrom<u32> for LineCap {
188+
type Error = anyhow::Error;
189+
190+
fn try_from(value: u32) -> Result<Self, Self::Error> {
191+
num_traits::FromPrimitive::from_u32(value)
192+
.with_context(|| format!("LineCap try_from::<u32>() for value {value} failed"))
193+
}
194+
}
195+
196+
impl From<LineCap> for piet::LineCap {
197+
fn from(value: LineCap) -> Self {
198+
match value {
199+
LineCap::Straight => piet::LineCap::Butt,
200+
LineCap::Rounded => piet::LineCap::Round,
201+
}
202+
}
203+
}
204+
205+
/// The overall style of the line
206+
#[derive(
207+
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, FromPrimitive, ToPrimitive,
208+
)]
209+
#[serde(rename = "line_style")]
210+
pub enum LineStyle {
211+
/// Solid line style
212+
#[default]
213+
#[serde(rename = "solid")]
214+
Solid,
215+
/// Dotted line style, the dots are equidistant
216+
#[serde(rename = "dotted")]
217+
Dotted,
218+
/// Dashed line style, the dashes have less space between them
219+
#[serde(rename = "dashed_narrow")]
220+
DashedNarrow,
221+
/// Dashed line style, the dashes are equidistant
222+
#[serde(rename = "dashed_equidistant")]
223+
DashedEquidistant,
224+
/// Dashed line style, the dashes have more space between them
225+
#[serde(rename = "dashed_wide")]
226+
DashedWide,
227+
}
228+
229+
impl LineStyle {
230+
/// Returns the baseline (meaning unscaled) dash pattern
231+
fn as_unscaled_vector(&self) -> Vec<f64> {
232+
match self {
233+
Self::Solid => Vec::new(),
234+
Self::Dotted => vec![0.0, 0.0], // LineCap must be set to 'Rounded'
235+
Self::DashedNarrow => vec![1.0, 0.618], // golden ratio, the longer segment is the dash itself
236+
Self::DashedEquidistant => vec![1.0, 1.0],
237+
Self::DashedWide => vec![1.0, 1.618], // golden ratio, the longer segment is the space between dashes
238+
}
239+
}
240+
/// Indicates whether or not the LineStyle is dotted
241+
pub fn is_dotted(&self) -> bool {
242+
match self {
243+
Self::Solid => false,
244+
Self::Dotted => true,
245+
Self::DashedNarrow => false,
246+
Self::DashedEquidistant => false,
247+
Self::DashedWide => false,
248+
}
249+
}
250+
}
251+
252+
impl TryFrom<u32> for LineStyle {
253+
type Error = anyhow::Error;
254+
255+
fn try_from(value: u32) -> Result<Self, Self::Error> {
256+
num_traits::FromPrimitive::from_u32(value)
257+
.with_context(|| format!("LineStyle try_from::<u32>() for value {value} failed"))
258+
}
259+
}

0 commit comments

Comments
 (0)