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