11// Imports
22use crate :: Color ;
33use crate :: style:: PressureCurve ;
4+ use anyhow:: Context ;
5+ use num_derive:: { FromPrimitive , ToPrimitive } ;
46use 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" ) ]
915pub 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
2439impl 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