kurbo/
stroke.rs

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