1
1
//! Bézier paths (up to cubic).
2
2
3
+ use std:: iter:: FromIterator ;
3
4
use std:: ops:: { Mul , Range } ;
4
5
5
6
use arrayvec:: ArrayVec ;
@@ -114,6 +115,79 @@ impl BezPath {
114
115
BezPath :: segments_of_slice ( & self . 0 )
115
116
}
116
117
118
+ /// Flatten the path, invoking the callback repeatedly.
119
+ ///
120
+ /// Flattening is the action of approximating a curve with a succession of line segments.
121
+ ///
122
+ /// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 30" height="30mm" width="120mm">
123
+ /// <path d="M26.7 24.94l.82-11.15M44.46 5.1L33.8 7.34" fill="none" stroke="#55d400" stroke-width=".5"/>
124
+ /// <path d="M26.7 24.94c.97-11.13 7.17-17.6 17.76-19.84M75.27 24.94l1.13-5.5 2.67-5.48 4-4.42L88 6.7l5.02-1.6" fill="none" stroke="#000"/>
125
+ /// <path d="M77.57 19.37a1.1 1.1 0 0 1-1.08 1.08 1.1 1.1 0 0 1-1.1-1.08 1.1 1.1 0 0 1 1.08-1.1 1.1 1.1 0 0 1 1.1 1.1" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
126
+ /// <path d="M77.57 19.37a1.1 1.1 0 0 1-1.08 1.08 1.1 1.1 0 0 1-1.1-1.08 1.1 1.1 0 0 1 1.08-1.1 1.1 1.1 0 0 1 1.1 1.1" color="#000" fill="#fff"/>
127
+ /// <path d="M80.22 13.93a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.08 1.1 1.1 0 0 1 1.08 1.08" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
128
+ /// <path d="M80.22 13.93a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.08 1.1 1.1 0 0 1 1.08 1.08" color="#000" fill="#fff"/>
129
+ /// <path d="M84.08 9.55a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.1-1.1 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
130
+ /// <path d="M84.08 9.55a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.1-1.1 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="#fff"/>
131
+ /// <path d="M89.1 6.66a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.08-1.08 1.1 1.1 0 0 1 1.1 1.08" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
132
+ /// <path d="M89.1 6.66a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.08-1.08 1.1 1.1 0 0 1 1.1 1.08" color="#000" fill="#fff"/>
133
+ /// <path d="M94.4 5a1.1 1.1 0 0 1-1.1 1.1A1.1 1.1 0 0 1 92.23 5a1.1 1.1 0 0 1 1.08-1.08A1.1 1.1 0 0 1 94.4 5" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
134
+ /// <path d="M94.4 5a1.1 1.1 0 0 1-1.1 1.1A1.1 1.1 0 0 1 92.23 5a1.1 1.1 0 0 1 1.08-1.08A1.1 1.1 0 0 1 94.4 5" color="#000" fill="#fff"/>
135
+ /// <path d="M76.44 25.13a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
136
+ /// <path d="M76.44 25.13a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="#fff"/>
137
+ /// <path d="M27.78 24.9a1.1 1.1 0 0 1-1.08 1.08 1.1 1.1 0 0 1-1.1-1.08 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
138
+ /// <path d="M27.78 24.9a1.1 1.1 0 0 1-1.08 1.08 1.1 1.1 0 0 1-1.1-1.08 1.1 1.1 0 0 1 1.1-1.1 1.1 1.1 0 0 1 1.08 1.1" color="#000" fill="#fff"/>
139
+ /// <path d="M45.4 5.14a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.1-1.1 1.1 1.1 0 0 1 1.1-1.08 1.1 1.1 0 0 1 1.1 1.08" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
140
+ /// <path d="M45.4 5.14a1.1 1.1 0 0 1-1.08 1.1 1.1 1.1 0 0 1-1.1-1.1 1.1 1.1 0 0 1 1.1-1.08 1.1 1.1 0 0 1 1.1 1.08" color="#000" fill="#fff"/>
141
+ /// <path d="M28.67 13.8a1.1 1.1 0 0 1-1.1 1.08 1.1 1.1 0 0 1-1.08-1.08 1.1 1.1 0 0 1 1.08-1.1 1.1 1.1 0 0 1 1.1 1.1" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
142
+ /// <path d="M28.67 13.8a1.1 1.1 0 0 1-1.1 1.08 1.1 1.1 0 0 1-1.08-1.08 1.1 1.1 0 0 1 1.08-1.1 1.1 1.1 0 0 1 1.1 1.1" color="#000" fill="#fff"/>
143
+ /// <path d="M35 7.32a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.1A1.1 1.1 0 0 1 35 7.33" color="#000" fill="none" stroke="#030303" stroke-linecap="round" stroke-opacity=".5"/>
144
+ /// <path d="M35 7.32a1.1 1.1 0 0 1-1.1 1.1 1.1 1.1 0 0 1-1.08-1.1 1.1 1.1 0 0 1 1.1-1.1A1.1 1.1 0 0 1 35 7.33" color="#000" fill="#fff"/>
145
+ /// <text style="line-height:6.61458302px" x="35.74" y="284.49" font-size="5.29" font-family="Sans" letter-spacing="0" word-spacing="0" fill="#b3b3b3" stroke-width=".26" transform="translate(19.595 -267)">
146
+ /// <tspan x="35.74" y="284.49" font-size="10.58">→</tspan>
147
+ /// </text>
148
+ /// </svg>
149
+ ///
150
+ /// The tolerance value controls the maximum distance between the curved input
151
+ /// segments and their polyline approximations. (In technical terms, this is the
152
+ /// Hausdorff distance). The algorithm attempts to bound this distance between
153
+ /// by `tolerance` but this is not absolutely guaranteed. The appropriate value
154
+ /// depends on the use, but for antialiasted rendering, a value of 0.25 has been
155
+ /// determined to give good results. The number of segments tends to scale as the
156
+ /// inverse square root of tolerance.
157
+ ///
158
+ /// <svg viewBox="0 0 47.5 13.2" height="100" width="350" xmlns="http://www.w3.org/2000/svg">
159
+ /// <path d="M-2.44 9.53c16.27-8.5 39.68-7.93 52.13 1.9" fill="none" stroke="#dde9af" stroke-width="4.6"/>
160
+ /// <path d="M-1.97 9.3C14.28 1.03 37.36 1.7 49.7 11.4" fill="none" stroke="#00d400" stroke-width=".57" stroke-linecap="round" stroke-dasharray="4.6, 2.291434"/>
161
+ /// <path d="M-1.94 10.46L6.2 6.08l28.32-1.4 15.17 6.74" fill="none" stroke="#000" stroke-width=".6"/>
162
+ /// <path d="M6.83 6.57a.9.9 0 0 1-1.25.15.9.9 0 0 1-.15-1.25.9.9 0 0 1 1.25-.15.9.9 0 0 1 .15 1.25" color="#000" stroke="#000" stroke-width=".57" stroke-linecap="round" stroke-opacity=".5"/>
163
+ /// <path d="M35.35 5.3a.9.9 0 0 1-1.25.15.9.9 0 0 1-.15-1.25.9.9 0 0 1 1.25-.15.9.9 0 0 1 .15 1.24" color="#000" stroke="#000" stroke-width=".6" stroke-opacity=".5"/>
164
+ /// <g fill="none" stroke="#ff7f2a" stroke-width=".26">
165
+ /// <path d="M20.4 3.8l.1 1.83M19.9 4.28l.48-.56.57.52M21.02 5.18l-.5.56-.6-.53" stroke-width=".2978872"/>
166
+ /// </g>
167
+ /// </svg>
168
+ ///
169
+ /// The callback will be called in order with each element of the generated
170
+ /// path. Because the result is made of polylines, these will be straight-line
171
+ /// path elements only, no curves.
172
+ ///
173
+ /// This algorithm is based on the blog post [Flattening quadratic Béziers]
174
+ /// but with some refinements. For one, there is a more careful approximation
175
+ /// at cusps. For two, the algorithm is extended to work with cubic Béziers
176
+ /// as well, by first subdividing into quadratics and then computing the
177
+ /// subdivision of each quadratic. However, as a clever trick, these quadratics
178
+ /// are subdivided fractionally, and their endpoints are not included.
179
+ ///
180
+ /// TODO: write a paper explaining this in more detail.
181
+ ///
182
+ /// Note: the [`flatten`](fn.flatten.html) function provides the same
183
+ /// functionality but works with slices and other [`PathEl`] iterators.
184
+ ///
185
+ /// [Flattening quadratic Béziers]: https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
186
+ /// [`PathEl`]: enum.PathEl.html
187
+ pub fn flatten ( & self , tolerance : f64 , callback : impl FnMut ( PathEl ) ) {
188
+ flatten ( self , tolerance, callback) ;
189
+ }
190
+
117
191
// TODO: expose as pub method? Maybe should be a trait so slice.segments() works?
118
192
fn segments_of_slice < ' a > ( slice : & ' a [ PathEl ] ) -> BezPathSegs < ' a > {
119
193
let first = match slice. get ( 0 ) {
@@ -194,17 +268,17 @@ impl BezPath {
194
268
}
195
269
}
196
270
197
- impl std :: iter :: FromIterator < PathEl > for BezPath {
271
+ impl FromIterator < PathEl > for BezPath {
198
272
fn from_iter < T : IntoIterator < Item = PathEl > > ( iter : T ) -> Self {
199
273
let el_vec: Vec < _ > = iter. into_iter ( ) . collect ( ) ;
200
274
BezPath :: from_vec ( el_vec)
201
275
}
202
276
}
203
277
204
- // this has weird semantics; signature assumes taking ownership but impl'd on a reference
205
- // NOTE: after removing this, we should impl IntoIterator for BezPath (with no reference)
206
- // and that impl should just call `self.0.into_iter()`
207
- # [ deprecated ( since = "0.5.6" , note = "use BezPath::iter instead" ) ]
278
+ /// Allow iteration over references to `BezPath`.
279
+ ///
280
+ /// Note: the semantics are slightly different than simply iterating over the
281
+ /// slice, as it returns `PathEl` items, rather than references.
208
282
impl < ' a > IntoIterator for & ' a BezPath {
209
283
type Item = PathEl ;
210
284
type IntoIter = std:: iter:: Cloned < std:: slice:: Iter < ' a , PathEl > > ;
@@ -214,6 +288,110 @@ impl<'a> IntoIterator for &'a BezPath {
214
288
}
215
289
}
216
290
291
+ impl IntoIterator for BezPath {
292
+ type Item = PathEl ;
293
+ type IntoIter = std:: vec:: IntoIter < PathEl > ;
294
+
295
+ fn into_iter ( self ) -> Self :: IntoIter {
296
+ self . 0 . into_iter ( )
297
+ }
298
+ }
299
+
300
+ /// Proportion of tolerance budget that goes to cubic to quadratic conversion.
301
+ const TO_QUAD_TOL : f64 = 0.1 ;
302
+
303
+ /// Flatten the path, invoking the callback repeatedly.
304
+ ///
305
+ /// See [`BezPath::flatten`](struct.BezPath.html#method.flatten) for more discussion.
306
+ /// This signature is a bit more general, allowing flattening of `&[PathEl]` slices
307
+ /// and other iterators yielding `PathEl`.
308
+ pub fn flatten (
309
+ path : impl IntoIterator < Item = PathEl > ,
310
+ tolerance : f64 ,
311
+ mut callback : impl FnMut ( PathEl ) ,
312
+ ) {
313
+ let sqrt_tol = tolerance. sqrt ( ) ;
314
+ let mut last_pt = None ;
315
+ let mut quad_buf = Vec :: new ( ) ;
316
+ for el in path {
317
+ match el {
318
+ PathEl :: MoveTo ( p) => {
319
+ last_pt = Some ( p) ;
320
+ callback ( PathEl :: MoveTo ( p) ) ;
321
+ }
322
+ PathEl :: LineTo ( p) => {
323
+ last_pt = Some ( p) ;
324
+ callback ( PathEl :: LineTo ( p) ) ;
325
+ }
326
+ PathEl :: QuadTo ( p1, p2) => {
327
+ if let Some ( p0) = last_pt {
328
+ let q = QuadBez :: new ( p0, p1, p2) ;
329
+ let params = q. estimate_subdiv ( sqrt_tol) ;
330
+ let n = ( ( 0.5 * params. val / sqrt_tol) . ceil ( ) as usize ) . max ( 1 ) ;
331
+ let step = 1.0 / ( n as f64 ) ;
332
+ for i in 1 ..( n - 1 ) {
333
+ let u = ( i as f64 ) * step;
334
+ let t = q. determine_subdiv_t ( & params, u) ;
335
+ let p = q. eval ( t) ;
336
+ callback ( PathEl :: LineTo ( p) ) ;
337
+ }
338
+ callback ( PathEl :: LineTo ( p2) ) ;
339
+ }
340
+ last_pt = Some ( p2) ;
341
+ }
342
+ PathEl :: CurveTo ( p1, p2, p3) => {
343
+ if let Some ( p0) = last_pt {
344
+ let c = CubicBez :: new ( p0, p1, p2, p3) ;
345
+
346
+ // Subdivide into quadratics, and estimate the number of
347
+ // subdivisions required for each, summing to arrive at an
348
+ // estimate for the number of subdivisions for the cubic.
349
+ // Also retain these parameters for later.
350
+ let iter = c. to_quads ( tolerance * TO_QUAD_TOL ) ;
351
+ quad_buf. clear ( ) ;
352
+ quad_buf. reserve ( iter. size_hint ( ) . 0 ) ;
353
+ let sqrt_remain_tol = sqrt_tol * ( 1.0 - TO_QUAD_TOL ) . sqrt ( ) ;
354
+ let mut sum = 0.0 ;
355
+ for ( _, _, q) in iter {
356
+ let params = q. estimate_subdiv ( sqrt_remain_tol) ;
357
+ sum += params. val ;
358
+ quad_buf. push ( ( q, params) ) ;
359
+ }
360
+ let n = ( ( 0.5 * sum / sqrt_remain_tol) . ceil ( ) as usize ) . max ( 1 ) ;
361
+
362
+ // Iterate through the quadratics, outputting the points of
363
+ // subdivisions that fall within that quadratic.
364
+ let step = sum / ( n as f64 ) ;
365
+ let mut i = 1 ;
366
+ let mut val_sum = 0.0 ;
367
+ for ( q, params) in & quad_buf {
368
+ let mut target = ( i as f64 ) * step;
369
+ let recip_val = params. val . recip ( ) ;
370
+ while target < val_sum + params. val {
371
+ let u = ( target - val_sum) * recip_val;
372
+ let t = q. determine_subdiv_t ( & params, u) ;
373
+ let p = q. eval ( t) ;
374
+ callback ( PathEl :: LineTo ( p) ) ;
375
+ i += 1 ;
376
+ if i == n + 1 {
377
+ break ;
378
+ }
379
+ target = ( i as f64 ) * step;
380
+ }
381
+ val_sum += params. val ;
382
+ }
383
+ callback ( PathEl :: LineTo ( p3) ) ;
384
+ }
385
+ last_pt = Some ( p3) ;
386
+ }
387
+ PathEl :: ClosePath => {
388
+ last_pt = None ;
389
+ callback ( PathEl :: ClosePath ) ;
390
+ }
391
+ }
392
+ }
393
+ }
394
+
217
395
impl Mul < PathEl > for Affine {
218
396
type Output = PathEl ;
219
397
0 commit comments