1 | // Copyright 2023 the Kurbo Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use core::{borrow::Borrow, f64::consts::PI}; |
5 | |
6 | use alloc::vec::Vec; |
7 | |
8 | use smallvec::SmallVec; |
9 | |
10 | #[cfg (not(feature = "std" ))] |
11 | use crate::common::FloatFuncs; |
12 | |
13 | use crate::{ |
14 | common::solve_quadratic, fit_to_bezpath, fit_to_bezpath_opt, offset::CubicOffset, Affine, Arc, |
15 | BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, QuadBez, Vec2, |
16 | }; |
17 | |
18 | /// Defines the connection between two segments of a stroke. |
19 | #[derive (Copy, Clone, PartialEq, Eq, Debug)] |
20 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
21 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
22 | pub enum Join { |
23 | /// A straight line connecting the segments. |
24 | Bevel, |
25 | /// The segments are extended to their natural intersection point. |
26 | Miter, |
27 | /// An arc between the segments. |
28 | Round, |
29 | } |
30 | |
31 | /// Defines the shape to be drawn at the ends of a stroke. |
32 | #[derive (Copy, Clone, PartialEq, Eq, Debug)] |
33 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
34 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
35 | pub enum Cap { |
36 | /// Flat cap. |
37 | Butt, |
38 | /// Square cap with dimensions equal to half the stroke width. |
39 | Square, |
40 | /// Rounded cap with radius equal to half the stroke width. |
41 | Round, |
42 | } |
43 | |
44 | /// Describes the visual style of a stroke. |
45 | #[derive (Clone, Debug)] |
46 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
47 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
48 | pub struct Stroke { |
49 | /// Width of the stroke. |
50 | pub width: f64, |
51 | /// Style for connecting segments of the stroke. |
52 | pub join: Join, |
53 | /// Limit for miter joins. |
54 | pub miter_limit: f64, |
55 | /// Style for capping the beginning of an open subpath. |
56 | pub start_cap: Cap, |
57 | /// Style for capping the end of an open subpath. |
58 | pub end_cap: Cap, |
59 | /// Lengths of dashes in alternating on/off order. |
60 | pub dash_pattern: Dashes, |
61 | /// Offset of the first dash. |
62 | pub dash_offset: f64, |
63 | } |
64 | |
65 | /// Options for path stroking. |
66 | pub struct StrokeOpts { |
67 | opt_level: StrokeOptLevel, |
68 | } |
69 | |
70 | /// Optimization level for computing |
71 | pub enum StrokeOptLevel { |
72 | /// Adaptively subdivide segments in half. |
73 | Subdivide, |
74 | /// Compute optimized subdivision points to minimize error. |
75 | Optimized, |
76 | } |
77 | |
78 | impl Default for StrokeOpts { |
79 | fn default() -> Self { |
80 | let opt_level: StrokeOptLevel = StrokeOptLevel::Subdivide; |
81 | StrokeOpts { opt_level } |
82 | } |
83 | } |
84 | |
85 | impl Default for Stroke { |
86 | fn default() -> Self { |
87 | Self { |
88 | width: 1.0, |
89 | join: Join::Round, |
90 | miter_limit: 4.0, |
91 | start_cap: Cap::Round, |
92 | end_cap: Cap::Round, |
93 | dash_pattern: Default::default(), |
94 | dash_offset: 0.0, |
95 | } |
96 | } |
97 | } |
98 | |
99 | impl Stroke { |
100 | /// Creates a new stroke with the specified width. |
101 | pub fn new(width: f64) -> Self { |
102 | Self { |
103 | width, |
104 | ..Default::default() |
105 | } |
106 | } |
107 | |
108 | /// Builder method for setting the join style. |
109 | pub fn with_join(mut self, join: Join) -> Self { |
110 | self.join = join; |
111 | self |
112 | } |
113 | |
114 | /// Builder method for setting the limit for miter joins. |
115 | pub fn with_miter_limit(mut self, limit: f64) -> Self { |
116 | self.miter_limit = limit; |
117 | self |
118 | } |
119 | |
120 | /// Builder method for setting the cap style for the start of the stroke. |
121 | pub fn with_start_cap(mut self, cap: Cap) -> Self { |
122 | self.start_cap = cap; |
123 | self |
124 | } |
125 | |
126 | /// Builder method for setting the cap style for the end of the stroke. |
127 | pub fn with_end_cap(mut self, cap: Cap) -> Self { |
128 | self.end_cap = cap; |
129 | self |
130 | } |
131 | |
132 | /// Builder method for setting the cap style. |
133 | pub fn with_caps(mut self, cap: Cap) -> Self { |
134 | self.start_cap = cap; |
135 | self.end_cap = cap; |
136 | self |
137 | } |
138 | |
139 | /// Builder method for setting the dashing parameters. |
140 | pub fn with_dashes<P>(mut self, offset: f64, pattern: P) -> Self |
141 | where |
142 | P: IntoIterator, |
143 | P::Item: Borrow<f64>, |
144 | { |
145 | self.dash_offset = offset; |
146 | self.dash_pattern.clear(); |
147 | self.dash_pattern |
148 | .extend(pattern.into_iter().map(|dash| *dash.borrow())); |
149 | self |
150 | } |
151 | } |
152 | |
153 | impl StrokeOpts { |
154 | /// Set optimization level for computing stroke outlines. |
155 | pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self { |
156 | self.opt_level = opt_level; |
157 | self |
158 | } |
159 | } |
160 | |
161 | /// Collection of values representing lengths in a dash pattern. |
162 | pub type Dashes = SmallVec<[f64; 4]>; |
163 | |
164 | /// Internal structure used for creating strokes. |
165 | #[derive (Default)] |
166 | struct StrokeCtx { |
167 | // As a possible future optimization, we might not need separate storage |
168 | // for forward and backward paths, we can add forward to the output in-place. |
169 | // However, this structure is clearer and the cost fairly modest. |
170 | output: BezPath, |
171 | forward_path: BezPath, |
172 | backward_path: BezPath, |
173 | start_pt: Point, |
174 | start_norm: Vec2, |
175 | start_tan: Vec2, |
176 | last_pt: Point, |
177 | last_tan: Vec2, |
178 | // Precomputation of the join threshold, to optimize per-join logic. |
179 | // If hypot < (hypot + dot) * join_thresh, omit join altogether. |
180 | join_thresh: f64, |
181 | } |
182 | |
183 | /// Expand a stroke into a fill. |
184 | /// |
185 | /// The `tolerance` parameter controls the accuracy of the result. In general, |
186 | /// the number of subdivisions in the output scales to the -1/6 power of the |
187 | /// parameter, for example making it 1/64 as big generates twice as many |
188 | /// segments. The appropriate value depends on the application; if the result |
189 | /// of the stroke will be scaled up, a smaller value is needed. |
190 | /// |
191 | /// This method attempts a fairly high degree of correctness, but ultimately |
192 | /// is based on computing parallel curves and adding joins and caps, rather than |
193 | /// computing the rigorously correct parallel sweep (which requires evolutes in |
194 | /// the general case). See [Nehab 2020] for more discussion. |
195 | /// |
196 | /// [Nehab 2020]: https://dl.acm.org/doi/10.1145/3386569.3392392 |
197 | pub fn stroke( |
198 | path: impl IntoIterator<Item = PathEl>, |
199 | style: &Stroke, |
200 | opts: &StrokeOpts, |
201 | tolerance: f64, |
202 | ) -> BezPath { |
203 | if style.dash_pattern.is_empty() { |
204 | stroke_undashed(path, style, tolerance, opts) |
205 | } else { |
206 | let dashed: impl Iterator = dash(inner:path.into_iter(), style.dash_offset, &style.dash_pattern); |
207 | stroke_undashed(path:dashed, style, tolerance, opts) |
208 | } |
209 | } |
210 | |
211 | /// Version of stroke expansion for styles with no dashes. |
212 | fn stroke_undashed( |
213 | path: impl IntoIterator<Item = PathEl>, |
214 | style: &Stroke, |
215 | tolerance: f64, |
216 | opts: &StrokeOpts, |
217 | ) -> BezPath { |
218 | let mut ctx = StrokeCtx { |
219 | join_thresh: 2.0 * tolerance / style.width, |
220 | ..Default::default() |
221 | }; |
222 | for el in path { |
223 | let p0 = ctx.last_pt; |
224 | match el { |
225 | PathEl::MoveTo(p) => { |
226 | ctx.finish(style); |
227 | ctx.start_pt = p; |
228 | ctx.last_pt = p; |
229 | } |
230 | PathEl::LineTo(p1) => { |
231 | if p1 != p0 { |
232 | let tangent = p1 - p0; |
233 | ctx.do_join(style, tangent); |
234 | ctx.last_tan = tangent; |
235 | ctx.do_line(style, tangent, p1); |
236 | } |
237 | } |
238 | PathEl::QuadTo(p1, p2) => { |
239 | if p1 != p0 || p2 != p0 { |
240 | let q = QuadBez::new(p0, p1, p2); |
241 | let (tan0, tan1) = PathSeg::Quad(q).tangents(); |
242 | ctx.do_join(style, tan0); |
243 | ctx.do_cubic(style, q.raise(), tolerance, opts); |
244 | ctx.last_tan = tan1; |
245 | } |
246 | } |
247 | PathEl::CurveTo(p1, p2, p3) => { |
248 | if p1 != p0 || p2 != p0 || p3 != p0 { |
249 | let c = CubicBez::new(p0, p1, p2, p3); |
250 | let (tan0, tan1) = PathSeg::Cubic(c).tangents(); |
251 | ctx.do_join(style, tan0); |
252 | ctx.do_cubic(style, c, tolerance, opts); |
253 | ctx.last_tan = tan1; |
254 | } |
255 | } |
256 | PathEl::ClosePath => { |
257 | if p0 != ctx.start_pt { |
258 | let tangent = ctx.start_pt - p0; |
259 | ctx.do_join(style, tangent); |
260 | ctx.last_tan = tangent; |
261 | ctx.do_line(style, tangent, ctx.start_pt); |
262 | } |
263 | ctx.finish_closed(style); |
264 | } |
265 | } |
266 | } |
267 | ctx.finish(style); |
268 | ctx.output |
269 | } |
270 | |
271 | fn round_cap(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2) { |
272 | round_join(out, tolerance, center, norm, PI); |
273 | } |
274 | |
275 | fn round_join(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { |
276 | let a: Affine = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); |
277 | let arc: Arc = Arc::new(center:Point::ORIGIN, (1.0, 1.0), PI - angle, angle, x_rotation:0.0); |
278 | arc.to_cubic_beziers(tolerance, |p1: Point, p2: Point, p3: Point| out.curve_to(p1:a * p1, p2:a * p2, p3:a * p3)); |
279 | } |
280 | |
281 | fn round_join_rev(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { |
282 | let a: Affine = Affine::new([norm.x, norm.y, norm.y, -norm.x, center.x, center.y]); |
283 | let arc: Arc = Arc::new(center:Point::ORIGIN, (1.0, 1.0), PI - angle, angle, x_rotation:0.0); |
284 | arc.to_cubic_beziers(tolerance, |p1: Point, p2: Point, p3: Point| out.curve_to(p1:a * p1, p2:a * p2, p3:a * p3)); |
285 | } |
286 | |
287 | fn square_cap(out: &mut BezPath, close: bool, center: Point, norm: Vec2) { |
288 | let a: Affine = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); |
289 | out.line_to(a * Point::new(x:1.0, y:1.0)); |
290 | out.line_to(a * Point::new(x:-1.0, y:1.0)); |
291 | if close { |
292 | out.close_path(); |
293 | } else { |
294 | out.line_to(a * Point::new(x:-1.0, y:0.0)); |
295 | } |
296 | } |
297 | |
298 | fn extend_reversed(out: &mut BezPath, elements: &[PathEl]) { |
299 | for i: usize in (1..elements.len()).rev() { |
300 | let end: Point = elements[i - 1].end_point().unwrap(); |
301 | match elements[i] { |
302 | PathEl::LineTo(_) => out.line_to(end), |
303 | PathEl::QuadTo(p1: Point, _) => out.quad_to(p1, p2:end), |
304 | PathEl::CurveTo(p1: Point, p2: Point, _) => out.curve_to(p1:p2, p2:p1, p3:end), |
305 | _ => unreachable!(), |
306 | } |
307 | } |
308 | } |
309 | |
310 | fn fit_with_opts(co: &CubicOffset, tolerance: f64, opts: &StrokeOpts) -> BezPath { |
311 | match opts.opt_level { |
312 | StrokeOptLevel::Subdivide => fit_to_bezpath(source:co, accuracy:tolerance), |
313 | StrokeOptLevel::Optimized => fit_to_bezpath_opt(source:co, accuracy:tolerance), |
314 | } |
315 | } |
316 | |
317 | impl StrokeCtx { |
318 | /// Append forward and backward paths to output. |
319 | fn finish(&mut self, style: &Stroke) { |
320 | // TODO: scale |
321 | let tolerance = 1e-3; |
322 | if self.forward_path.is_empty() { |
323 | return; |
324 | } |
325 | self.output.extend(&self.forward_path); |
326 | let back_els = self.backward_path.elements(); |
327 | let return_p = back_els[back_els.len() - 1].end_point().unwrap(); |
328 | let d = self.last_pt - return_p; |
329 | match style.end_cap { |
330 | Cap::Butt => self.output.line_to(return_p), |
331 | Cap::Round => round_cap(&mut self.output, tolerance, self.last_pt, d), |
332 | Cap::Square => square_cap(&mut self.output, false, self.last_pt, d), |
333 | } |
334 | extend_reversed(&mut self.output, back_els); |
335 | match style.start_cap { |
336 | Cap::Butt => self.output.close_path(), |
337 | Cap::Round => round_cap(&mut self.output, tolerance, self.start_pt, self.start_norm), |
338 | Cap::Square => square_cap(&mut self.output, true, self.start_pt, self.start_norm), |
339 | } |
340 | |
341 | self.forward_path.truncate(0); |
342 | self.backward_path.truncate(0); |
343 | } |
344 | |
345 | /// Finish a closed path |
346 | fn finish_closed(&mut self, style: &Stroke) { |
347 | if self.forward_path.is_empty() { |
348 | return; |
349 | } |
350 | self.do_join(style, self.start_tan); |
351 | self.output.extend(&self.forward_path); |
352 | self.output.close_path(); |
353 | let back_els = self.backward_path.elements(); |
354 | let last_pt = back_els[back_els.len() - 1].end_point().unwrap(); |
355 | self.output.move_to(last_pt); |
356 | extend_reversed(&mut self.output, back_els); |
357 | self.output.close_path(); |
358 | self.forward_path.truncate(0); |
359 | self.backward_path.truncate(0); |
360 | } |
361 | |
362 | fn do_join(&mut self, style: &Stroke, tan0: Vec2) { |
363 | // TODO: scale |
364 | let tolerance = 1e-3; |
365 | let scale = 0.5 * style.width / tan0.hypot(); |
366 | let norm = scale * Vec2::new(-tan0.y, tan0.x); |
367 | let p0 = self.last_pt; |
368 | if self.forward_path.elements().is_empty() { |
369 | self.forward_path.move_to(p0 - norm); |
370 | self.backward_path.move_to(p0 + norm); |
371 | self.start_tan = tan0; |
372 | self.start_norm = norm; |
373 | } else { |
374 | let ab = self.last_tan; |
375 | let cd = tan0; |
376 | let cross = ab.cross(cd); |
377 | let dot = ab.dot(cd); |
378 | let hypot = cross.hypot(dot); |
379 | // possible TODO: a minor speedup could be squaring both sides |
380 | if dot <= 0.0 || cross.abs() >= hypot * self.join_thresh { |
381 | match style.join { |
382 | Join::Bevel => { |
383 | self.forward_path.line_to(p0 - norm); |
384 | self.backward_path.line_to(p0 + norm); |
385 | } |
386 | Join::Miter => { |
387 | if 2.0 * hypot < (hypot + dot) * style.miter_limit.powi(2) { |
388 | // TODO: maybe better to store last_norm or derive from path? |
389 | let last_scale = 0.5 * style.width / ab.hypot(); |
390 | let last_norm = last_scale * Vec2::new(-ab.y, ab.x); |
391 | if cross > 0.0 { |
392 | let fp_last = p0 - last_norm; |
393 | let fp_this = p0 - norm; |
394 | let h = ab.cross(fp_this - fp_last) / cross; |
395 | let miter_pt = fp_this - cd * h; |
396 | self.forward_path.line_to(miter_pt); |
397 | } else if cross < 0.0 { |
398 | let fp_last = p0 + last_norm; |
399 | let fp_this = p0 + norm; |
400 | let h = ab.cross(fp_this - fp_last) / cross; |
401 | let miter_pt = fp_this - cd * h; |
402 | self.backward_path.line_to(miter_pt); |
403 | } |
404 | } |
405 | self.forward_path.line_to(p0 - norm); |
406 | self.backward_path.line_to(p0 + norm); |
407 | } |
408 | Join::Round => { |
409 | let angle = cross.atan2(dot); |
410 | if angle > 0.0 { |
411 | self.backward_path.line_to(p0 + norm); |
412 | round_join(&mut self.forward_path, tolerance, p0, norm, angle); |
413 | } else { |
414 | self.forward_path.line_to(p0 - norm); |
415 | round_join_rev(&mut self.backward_path, tolerance, p0, -norm, -angle); |
416 | } |
417 | } |
418 | } |
419 | } |
420 | } |
421 | } |
422 | |
423 | fn do_line(&mut self, style: &Stroke, tangent: Vec2, p1: Point) { |
424 | let scale = 0.5 * style.width / tangent.hypot(); |
425 | let norm = scale * Vec2::new(-tangent.y, tangent.x); |
426 | self.forward_path.line_to(p1 - norm); |
427 | self.backward_path.line_to(p1 + norm); |
428 | self.last_pt = p1; |
429 | } |
430 | |
431 | fn do_cubic(&mut self, style: &Stroke, c: CubicBez, tolerance: f64, opts: &StrokeOpts) { |
432 | // First, detect degenerate linear case |
433 | |
434 | // Ordinarily, this is the direction of the chord, but if the chord is very |
435 | // short, we take the longer control arm. |
436 | let chord = c.p3 - c.p0; |
437 | let mut chord_ref = chord; |
438 | let mut chord_ref_hypot2 = chord_ref.hypot2(); |
439 | let d01 = c.p1 - c.p0; |
440 | if d01.hypot2() > chord_ref_hypot2 { |
441 | chord_ref = d01; |
442 | chord_ref_hypot2 = chord_ref.hypot2(); |
443 | } |
444 | let d23 = c.p3 - c.p2; |
445 | if d23.hypot2() > chord_ref_hypot2 { |
446 | chord_ref = d23; |
447 | chord_ref_hypot2 = chord_ref.hypot2(); |
448 | } |
449 | // Project Bézier onto chord |
450 | let p0 = c.p0.to_vec2().dot(chord_ref); |
451 | let p1 = c.p1.to_vec2().dot(chord_ref); |
452 | let p2 = c.p2.to_vec2().dot(chord_ref); |
453 | let p3 = c.p3.to_vec2().dot(chord_ref); |
454 | const ENDPOINT_D: f64 = 0.01; |
455 | if p3 <= p0 |
456 | || p1 > p2 |
457 | || p1 < p0 + ENDPOINT_D * (p3 - p0) |
458 | || p2 > p3 - ENDPOINT_D * (p3 - p0) |
459 | { |
460 | // potentially a cusp inside |
461 | let x01 = d01.cross(chord_ref); |
462 | let x23 = d23.cross(chord_ref); |
463 | let x03 = chord.cross(chord_ref); |
464 | let thresh = tolerance.powi(2) * chord_ref_hypot2; |
465 | if x01 * x01 < thresh && x23 * x23 < thresh && x03 * x03 < thresh { |
466 | // control points are nearly co-linear |
467 | let midpoint = c.p0.midpoint(c.p3); |
468 | // Mapping back from projection of reference chord |
469 | let ref_vec = chord_ref / chord_ref_hypot2; |
470 | let ref_pt = midpoint - 0.5 * (p0 + p3) * ref_vec; |
471 | self.do_linear(style, c, [p0, p1, p2, p3], ref_pt, ref_vec); |
472 | return; |
473 | } |
474 | } |
475 | |
476 | // A tuning parameter for regularization. A value too large may distort the curve, |
477 | // while a value too small may fail to generate smooth curves. This is a somewhat |
478 | // arbitrary value, and should be revisited. |
479 | const DIM_TUNE: f64 = 0.25; |
480 | let dimension = tolerance * DIM_TUNE; |
481 | let co = CubicOffset::new_regularized(c, -0.5 * style.width, dimension); |
482 | let forward = fit_with_opts(&co, tolerance, opts); |
483 | self.forward_path.extend(forward.into_iter().skip(1)); |
484 | let co = CubicOffset::new_regularized(c, 0.5 * style.width, dimension); |
485 | let backward = fit_with_opts(&co, tolerance, opts); |
486 | self.backward_path.extend(backward.into_iter().skip(1)); |
487 | self.last_pt = c.p3; |
488 | } |
489 | |
490 | /// Do a cubic which is actually linear. |
491 | /// |
492 | /// The `p` argument is the control points projected to the reference chord. |
493 | /// The ref arguments are the inverse map of a projection back to the client |
494 | /// coordinate space. |
495 | fn do_linear( |
496 | &mut self, |
497 | style: &Stroke, |
498 | c: CubicBez, |
499 | p: [f64; 4], |
500 | ref_pt: Point, |
501 | ref_vec: Vec2, |
502 | ) { |
503 | // Always do round join, to model cusp as limit of finite curvature (see Nehab). |
504 | let style = Stroke::new(style.width).with_join(Join::Round); |
505 | // Tangents of endpoints (for connecting to joins) |
506 | let (tan0, tan1) = PathSeg::Cubic(c).tangents(); |
507 | self.last_tan = tan0; |
508 | // find cusps |
509 | let c0 = p[1] - p[0]; |
510 | let c1 = 2.0 * p[2] - 4.0 * p[1] + 2.0 * p[0]; |
511 | let c2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0]; |
512 | let roots = solve_quadratic(c0, c1, c2); |
513 | // discard cusps right at endpoints |
514 | const EPSILON: f64 = 1e-6; |
515 | for t in roots { |
516 | if t > EPSILON && t < 1.0 - EPSILON { |
517 | let mt = 1.0 - t; |
518 | let z = mt * (mt * mt * p[0] + 3.0 * t * (mt * p[1] + t * p[2])) + t * t * t * p[3]; |
519 | let p = ref_pt + z * ref_vec; |
520 | let tan = p - self.last_pt; |
521 | self.do_join(&style, tan); |
522 | self.do_line(&style, tan, p); |
523 | self.last_tan = tan; |
524 | } |
525 | } |
526 | let tan = c.p3 - self.last_pt; |
527 | self.do_join(&style, tan); |
528 | self.do_line(&style, tan, c.p3); |
529 | self.last_tan = tan; |
530 | self.do_join(&style, tan1); |
531 | } |
532 | } |
533 | |
534 | /// An implementation of dashing as an iterator-to-iterator transformation. |
535 | #[doc (hidden)] |
536 | pub struct DashIterator<'a, T> { |
537 | inner: T, |
538 | input_done: bool, |
539 | closepath_pending: bool, |
540 | dashes: &'a [f64], |
541 | dash_ix: usize, |
542 | init_dash_ix: usize, |
543 | init_dash_remaining: f64, |
544 | init_is_active: bool, |
545 | is_active: bool, |
546 | state: DashState, |
547 | current_seg: PathSeg, |
548 | t: f64, |
549 | dash_remaining: f64, |
550 | seg_remaining: f64, |
551 | start_pt: Point, |
552 | last_pt: Point, |
553 | stash: Vec<PathEl>, |
554 | stash_ix: usize, |
555 | } |
556 | |
557 | #[derive (PartialEq, Eq)] |
558 | enum DashState { |
559 | NeedInput, |
560 | ToStash, |
561 | Working, |
562 | FromStash, |
563 | } |
564 | |
565 | impl<'a, T: Iterator<Item = PathEl>> Iterator for DashIterator<'a, T> { |
566 | type Item = PathEl; |
567 | |
568 | fn next(&mut self) -> Option<PathEl> { |
569 | loop { |
570 | match self.state { |
571 | DashState::NeedInput => { |
572 | if self.input_done { |
573 | return None; |
574 | } |
575 | self.get_input(); |
576 | if self.input_done { |
577 | return None; |
578 | } |
579 | self.state = DashState::ToStash; |
580 | } |
581 | DashState::ToStash => { |
582 | if let Some(el) = self.step() { |
583 | self.stash.push(el); |
584 | } |
585 | } |
586 | DashState::Working => { |
587 | if let Some(el) = self.step() { |
588 | return Some(el); |
589 | } |
590 | } |
591 | DashState::FromStash => { |
592 | if let Some(el) = self.stash.get(self.stash_ix) { |
593 | self.stash_ix += 1; |
594 | return Some(*el); |
595 | } else { |
596 | self.stash.clear(); |
597 | self.stash_ix = 0; |
598 | if self.input_done { |
599 | return None; |
600 | } |
601 | if self.closepath_pending { |
602 | self.closepath_pending = false; |
603 | self.state = DashState::NeedInput; |
604 | } else { |
605 | self.state = DashState::ToStash; |
606 | } |
607 | } |
608 | } |
609 | } |
610 | } |
611 | } |
612 | } |
613 | |
614 | fn seg_to_el(el: &PathSeg) -> PathEl { |
615 | match el { |
616 | PathSeg::Line(l: &Line) => PathEl::LineTo(l.p1), |
617 | PathSeg::Quad(q: &QuadBez) => PathEl::QuadTo(q.p1, q.p2), |
618 | PathSeg::Cubic(c: &CubicBez) => PathEl::CurveTo(c.p1, c.p2, c.p3), |
619 | } |
620 | } |
621 | |
622 | const DASH_ACCURACY: f64 = 1e-6; |
623 | |
624 | /// Create a new dashing iterator. |
625 | /// |
626 | /// Handling of dashes is fairly orthogonal to stroke expansion. This iterator |
627 | /// is an internal detail of the stroke expansion logic, but is also available |
628 | /// separately, and is expected to be useful when doing stroke expansion on |
629 | /// GPU. |
630 | /// |
631 | /// It is implemented as an iterator-to-iterator transform. Because it consumes |
632 | /// the input sequentially and produces consistent output with correct joins, |
633 | /// it requires internal state and may allocate. |
634 | /// |
635 | /// Accuracy is currently hard-coded to 1e-6. This is better than generally |
636 | /// expected, and care is taken to get cusps correct, among other things. |
637 | pub fn dash<'a>( |
638 | inner: impl Iterator<Item = PathEl> + 'a, |
639 | dash_offset: f64, |
640 | dashes: &'a [f64], |
641 | ) -> impl Iterator<Item = PathEl> + 'a { |
642 | dash_impl(inner, dash_offset, dashes) |
643 | } |
644 | |
645 | // This is only a separate function to make `DashIterator::new()` typecheck. |
646 | fn dash_impl<T: Iterator<Item = PathEl>>( |
647 | inner: T, |
648 | dash_offset: f64, |
649 | dashes: &[f64], |
650 | ) -> DashIterator<T> { |
651 | let mut dash_ix = 0; |
652 | let mut dash_remaining = dashes[dash_ix] - dash_offset; |
653 | let mut is_active = true; |
654 | // Find place in dashes array for initial offset. |
655 | while dash_remaining < 0.0 { |
656 | dash_ix = (dash_ix + 1) % dashes.len(); |
657 | dash_remaining += dashes[dash_ix]; |
658 | is_active = !is_active; |
659 | } |
660 | DashIterator { |
661 | inner, |
662 | input_done: false, |
663 | closepath_pending: false, |
664 | dashes, |
665 | dash_ix, |
666 | init_dash_ix: dash_ix, |
667 | init_dash_remaining: dash_remaining, |
668 | init_is_active: is_active, |
669 | is_active, |
670 | state: DashState::NeedInput, |
671 | current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)), |
672 | t: 0.0, |
673 | dash_remaining, |
674 | seg_remaining: 0.0, |
675 | start_pt: Point::ORIGIN, |
676 | last_pt: Point::ORIGIN, |
677 | stash: Vec::new(), |
678 | stash_ix: 0, |
679 | } |
680 | } |
681 | |
682 | impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> { |
683 | #[doc (hidden)] |
684 | #[deprecated (since = "0.10.4" , note = "use dash() instead" )] |
685 | pub fn new(inner: T, dash_offset: f64, dashes: &'a [f64]) -> Self { |
686 | dash_impl(inner, dash_offset, dashes) |
687 | } |
688 | |
689 | fn get_input(&mut self) { |
690 | loop { |
691 | if self.closepath_pending { |
692 | self.handle_closepath(); |
693 | break; |
694 | } |
695 | let Some(next_el) = self.inner.next() else { |
696 | self.input_done = true; |
697 | self.state = DashState::FromStash; |
698 | return; |
699 | }; |
700 | let p0 = self.last_pt; |
701 | match next_el { |
702 | PathEl::MoveTo(p) => { |
703 | if !self.stash.is_empty() { |
704 | self.state = DashState::FromStash; |
705 | } |
706 | self.start_pt = p; |
707 | self.last_pt = p; |
708 | self.reset_phase(); |
709 | continue; |
710 | } |
711 | PathEl::LineTo(p1) => { |
712 | let l = Line::new(p0, p1); |
713 | self.seg_remaining = l.arclen(DASH_ACCURACY); |
714 | self.current_seg = PathSeg::Line(l); |
715 | self.last_pt = p1; |
716 | } |
717 | PathEl::QuadTo(p1, p2) => { |
718 | let q = QuadBez::new(p0, p1, p2); |
719 | self.seg_remaining = q.arclen(DASH_ACCURACY); |
720 | self.current_seg = PathSeg::Quad(q); |
721 | self.last_pt = p2; |
722 | } |
723 | PathEl::CurveTo(p1, p2, p3) => { |
724 | let c = CubicBez::new(p0, p1, p2, p3); |
725 | self.seg_remaining = c.arclen(DASH_ACCURACY); |
726 | self.current_seg = PathSeg::Cubic(c); |
727 | self.last_pt = p3; |
728 | } |
729 | PathEl::ClosePath => { |
730 | self.closepath_pending = true; |
731 | if p0 != self.start_pt { |
732 | let l = Line::new(p0, self.start_pt); |
733 | self.seg_remaining = l.arclen(DASH_ACCURACY); |
734 | self.current_seg = PathSeg::Line(l); |
735 | self.last_pt = self.start_pt; |
736 | } else { |
737 | self.handle_closepath(); |
738 | } |
739 | } |
740 | } |
741 | break; |
742 | } |
743 | self.t = 0.0; |
744 | } |
745 | |
746 | /// Move arc length forward to next event. |
747 | fn step(&mut self) -> Option<PathEl> { |
748 | let mut result = None; |
749 | if self.state == DashState::ToStash && self.stash.is_empty() { |
750 | if self.is_active { |
751 | result = Some(PathEl::MoveTo(self.current_seg.start())); |
752 | } else { |
753 | self.state = DashState::Working; |
754 | } |
755 | } else if self.dash_remaining < self.seg_remaining { |
756 | // next transition is a dash transition |
757 | let seg = self.current_seg.subsegment(self.t..1.0); |
758 | let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY); |
759 | if self.is_active { |
760 | let subseg = seg.subsegment(0.0..t1); |
761 | result = Some(seg_to_el(&subseg)); |
762 | self.state = DashState::Working; |
763 | } else { |
764 | let p = seg.eval(t1); |
765 | result = Some(PathEl::MoveTo(p)); |
766 | } |
767 | self.is_active = !self.is_active; |
768 | self.t += t1 * (1.0 - self.t); |
769 | self.seg_remaining -= self.dash_remaining; |
770 | self.dash_ix += 1; |
771 | if self.dash_ix == self.dashes.len() { |
772 | self.dash_ix = 0; |
773 | } |
774 | self.dash_remaining = self.dashes[self.dash_ix]; |
775 | } else { |
776 | if self.is_active { |
777 | let seg = self.current_seg.subsegment(self.t..1.0); |
778 | result = Some(seg_to_el(&seg)); |
779 | } |
780 | self.dash_remaining -= self.seg_remaining; |
781 | self.get_input(); |
782 | } |
783 | result |
784 | } |
785 | |
786 | fn handle_closepath(&mut self) { |
787 | if self.state == DashState::ToStash { |
788 | // Have looped back without breaking a dash, just play it back |
789 | self.stash.push(PathEl::ClosePath); |
790 | } else if self.is_active { |
791 | // connect with path in stash, skip MoveTo. |
792 | self.stash_ix = 1; |
793 | } |
794 | self.state = DashState::FromStash; |
795 | self.reset_phase(); |
796 | } |
797 | |
798 | fn reset_phase(&mut self) { |
799 | self.dash_ix = self.init_dash_ix; |
800 | self.dash_remaining = self.init_dash_remaining; |
801 | self.is_active = self.init_is_active; |
802 | } |
803 | } |
804 | |
805 | #[cfg (test)] |
806 | mod tests { |
807 | use crate::{ |
808 | dash, segments, stroke, Cap::Butt, CubicBez, Join::Miter, Line, PathSeg, Shape, Stroke, |
809 | }; |
810 | |
811 | // A degenerate stroke with a cusp at the endpoint. |
812 | #[test ] |
813 | fn pathological_stroke() { |
814 | let curve = CubicBez::new( |
815 | (602.469, 286.585), |
816 | (641.975, 286.585), |
817 | (562.963, 286.585), |
818 | (562.963, 286.585), |
819 | ); |
820 | let path = curve.into_path(0.1); |
821 | let stroke_style = Stroke::new(1.); |
822 | let stroked = stroke(path, &stroke_style, &Default::default(), 0.001); |
823 | assert!(stroked.is_finite()); |
824 | } |
825 | |
826 | // Test cases adapted from https://github.com/linebender/vello/pull/388 |
827 | #[test ] |
828 | fn broken_strokes() { |
829 | let broken_cubics = [ |
830 | [ |
831 | (465.24423, 107.11105), |
832 | (475.50754, 107.11105), |
833 | (475.50754, 107.11105), |
834 | (475.50754, 107.11105), |
835 | ], |
836 | [(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], // Near-cusp |
837 | [(0., 0.), (0., -10.), (0., -10.), (0., 10.)], // Flat line with 180 |
838 | [(10., 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s |
839 | [(39., -39.), (40., -40.), (40., -40.), (0., 0.)], // Flat diagonal with 180 |
840 | [(40., 40.), (0., 0.), (200., 200.), (0., 0.)], // Diag w/ an internal 180 |
841 | [(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], // Circle |
842 | // Flat line with no turns: |
843 | [ |
844 | (400.75, 100.05), |
845 | (400.75, 100.05), |
846 | (100.05, 300.95), |
847 | (100.05, 300.95), |
848 | ], |
849 | [(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s |
850 | [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], // Flat line with a 180 |
851 | ]; |
852 | let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter); |
853 | for cubic in &broken_cubics { |
854 | let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1); |
855 | let stroked = stroke(path, &stroke_style, &Default::default(), 0.001); |
856 | assert!(stroked.is_finite()); |
857 | } |
858 | } |
859 | |
860 | #[test ] |
861 | fn dash_sequence() { |
862 | let shape = Line::new((0.0, 0.0), (21.0, 0.0)); |
863 | let dashes = [1., 5., 2., 5.]; |
864 | let expansion = [ |
865 | PathSeg::Line(Line::new((6., 0.), (8., 0.))), |
866 | PathSeg::Line(Line::new((13., 0.), (14., 0.))), |
867 | PathSeg::Line(Line::new((19., 0.), (21., 0.))), |
868 | PathSeg::Line(Line::new((0., 0.), (1., 0.))), |
869 | ]; |
870 | let iter = segments(dash(shape.path_elements(0.), 0., &dashes)); |
871 | assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion); |
872 | } |
873 | |
874 | #[test ] |
875 | fn dash_sequence_offset() { |
876 | // Same as dash_sequence, but with a dash offset |
877 | // of 3, which skips the first dash and cuts into |
878 | // the first gap. |
879 | let shape = Line::new((0.0, 0.0), (21.0, 0.0)); |
880 | let dashes = [1., 5., 2., 5.]; |
881 | let expansion = [ |
882 | PathSeg::Line(Line::new((3., 0.), (5., 0.))), |
883 | PathSeg::Line(Line::new((10., 0.), (11., 0.))), |
884 | PathSeg::Line(Line::new((16., 0.), (18., 0.))), |
885 | ]; |
886 | let iter = segments(dash(shape.path_elements(0.), 3., &dashes)); |
887 | assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion); |
888 | } |
889 | } |
890 | |