Coverage Report

Created: 2025-11-16 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.12.0/src/stroke.rs
Line
Count
Source
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, 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))]
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
/// 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))]
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
#[derive(Clone, Copy, Debug, PartialEq)]
67
pub 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)]
79
pub enum StrokeOptLevel {
80
    /// Adaptively subdivide segments in half.
81
    Subdivide,
82
    /// Compute optimized subdivision points to minimize error.
83
    Optimized,
84
}
85
86
impl Default for StrokeOpts {
87
0
    fn default() -> Self {
88
0
        let opt_level = StrokeOptLevel::Subdivide;
89
0
        StrokeOpts { opt_level }
90
0
    }
91
}
92
93
impl Default for Stroke {
94
0
    fn default() -> Self {
95
0
        Self {
96
0
            width: 1.0,
97
0
            join: Join::Round,
98
0
            miter_limit: 4.0,
99
0
            start_cap: Cap::Round,
100
0
            end_cap: Cap::Round,
101
0
            dash_pattern: SmallVec::default(),
102
0
            dash_offset: 0.0,
103
0
        }
104
0
    }
105
}
106
107
impl Stroke {
108
    /// Creates a new stroke with the specified width.
109
0
    pub fn new(width: f64) -> Self {
110
0
        Self {
111
0
            width,
112
0
            ..Stroke::default()
113
0
        }
114
0
    }
115
116
    /// Builder method for setting the join style.
117
0
    pub fn with_join(mut self, join: Join) -> Self {
118
0
        self.join = join;
119
0
        self
120
0
    }
121
122
    /// Builder method for setting the limit for miter joins.
123
0
    pub fn with_miter_limit(mut self, limit: f64) -> Self {
124
0
        self.miter_limit = limit;
125
0
        self
126
0
    }
127
128
    /// Builder method for setting the cap style for the start of the stroke.
129
0
    pub fn with_start_cap(mut self, cap: Cap) -> Self {
130
0
        self.start_cap = cap;
131
0
        self
132
0
    }
133
134
    /// Builder method for setting the cap style for the end of the stroke.
135
0
    pub fn with_end_cap(mut self, cap: Cap) -> Self {
136
0
        self.end_cap = cap;
137
0
        self
138
0
    }
139
140
    /// Builder method for setting the cap style.
141
0
    pub fn with_caps(mut self, cap: Cap) -> Self {
142
0
        self.start_cap = cap;
143
0
        self.end_cap = cap;
144
0
        self
145
0
    }
146
147
    /// Builder method for setting the dashing parameters.
148
0
    pub fn with_dashes<P>(mut self, offset: f64, pattern: P) -> Self
149
0
    where
150
0
        P: IntoIterator,
151
0
        P::Item: Borrow<f64>,
152
    {
153
0
        self.dash_offset = offset;
154
0
        self.dash_pattern.clear();
155
0
        self.dash_pattern
156
0
            .extend(pattern.into_iter().map(|dash| *dash.borrow()));
157
0
        self
158
0
    }
159
}
160
161
impl StrokeOpts {
162
    /// Set optimization level for computing stroke outlines.
163
0
    pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self {
164
0
        self.opt_level = opt_level;
165
0
        self
166
0
    }
167
}
168
169
/// Collection of values representing lengths in a dash pattern.
170
pub 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)]
176
pub 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
194
impl StrokeCtx {
195
    /// Return the path that defines the expanded stroke.
196
0
    pub fn output(&self) -> &BezPath {
197
0
        &self.output
198
0
    }
199
}
200
201
impl StrokeCtx {
202
0
    fn reset(&mut self) {
203
0
        self.output.truncate(0);
204
0
        self.forward_path.truncate(0);
205
0
        self.backward_path.truncate(0);
206
0
        self.start_pt = Point::default();
207
0
        self.start_norm = Vec2::default();
208
0
        self.start_tan = Vec2::default();
209
0
        self.last_pt = Point::default();
210
0
        self.last_tan = Vec2::default();
211
0
        self.join_thresh = 0.0;
212
0
    }
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
232
0
pub fn stroke(
233
0
    path: impl IntoIterator<Item = PathEl>,
234
0
    style: &Stroke,
235
0
    _opts: &StrokeOpts,
236
0
    tolerance: f64,
237
0
) -> BezPath {
238
0
    let mut ctx = StrokeCtx::default();
239
0
    stroke_with(path, style, _opts, tolerance, &mut ctx);
240
241
0
    ctx.output
242
0
}
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`].
253
0
pub fn stroke_with(
254
0
    path: impl IntoIterator<Item = PathEl>,
255
0
    style: &Stroke,
256
0
    _opts: &StrokeOpts,
257
0
    tolerance: f64,
258
0
    ctx: &mut StrokeCtx,
259
0
) {
260
0
    if style.dash_pattern.is_empty() {
261
0
        stroke_undashed(path, style, tolerance, ctx);
262
0
    } else {
263
0
        let dashed = dash(path.into_iter(), style.dash_offset, &style.dash_pattern);
264
0
        stroke_undashed(dashed, style, tolerance, ctx);
265
0
    }
266
0
}
267
268
/// Version of stroke expansion for styles with no dashes.
269
0
fn stroke_undashed(
270
0
    path: impl IntoIterator<Item = PathEl>,
271
0
    style: &Stroke,
272
0
    tolerance: f64,
273
0
    ctx: &mut StrokeCtx,
274
0
) {
275
0
    ctx.reset();
276
0
    ctx.join_thresh = 2.0 * tolerance / style.width;
277
278
0
    for el in path {
279
0
        let p0 = ctx.last_pt;
280
0
        match el {
281
0
            PathEl::MoveTo(p) => {
282
0
                ctx.finish(style);
283
0
                ctx.start_pt = p;
284
0
                ctx.last_pt = p;
285
0
            }
286
0
            PathEl::LineTo(p1) => {
287
0
                if p1 != p0 {
288
0
                    let tangent = p1 - p0;
289
0
                    ctx.do_join(style, tangent);
290
0
                    ctx.last_tan = tangent;
291
0
                    ctx.do_line(style, tangent, p1);
292
0
                }
293
            }
294
0
            PathEl::QuadTo(p1, p2) => {
295
0
                if p1 != p0 || p2 != p0 {
296
0
                    let q = QuadBez::new(p0, p1, p2);
297
0
                    let (tan0, tan1) = PathSeg::Quad(q).tangents();
298
0
                    ctx.do_join(style, tan0);
299
0
                    ctx.do_cubic(style, q.raise(), tolerance);
300
0
                    ctx.last_tan = tan1;
301
0
                }
302
            }
303
0
            PathEl::CurveTo(p1, p2, p3) => {
304
0
                if p1 != p0 || p2 != p0 || p3 != p0 {
305
0
                    let c = CubicBez::new(p0, p1, p2, p3);
306
0
                    let (tan0, tan1) = PathSeg::Cubic(c).tangents();
307
0
                    ctx.do_join(style, tan0);
308
0
                    ctx.do_cubic(style, c, tolerance);
309
0
                    ctx.last_tan = tan1;
310
0
                }
311
            }
312
            PathEl::ClosePath => {
313
0
                if p0 != ctx.start_pt {
314
0
                    let tangent = ctx.start_pt - p0;
315
0
                    ctx.do_join(style, tangent);
316
0
                    ctx.last_tan = tangent;
317
0
                    ctx.do_line(style, tangent, ctx.start_pt);
318
0
                }
319
0
                ctx.finish_closed(style);
320
            }
321
        }
322
    }
323
0
    ctx.finish(style);
324
0
}
325
326
0
fn round_cap(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2) {
327
0
    round_join(out, tolerance, center, norm, PI);
328
0
}
329
330
0
fn round_join(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) {
331
0
    let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]);
332
0
    let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0);
333
0
    arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3));
334
0
}
335
336
0
fn round_join_rev(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) {
337
0
    let a = Affine::new([norm.x, norm.y, norm.y, -norm.x, center.x, center.y]);
338
0
    let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0);
339
0
    arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3));
340
0
}
341
342
0
fn square_cap(out: &mut BezPath, close: bool, center: Point, norm: Vec2) {
343
0
    let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]);
344
0
    out.line_to(a * Point::new(1.0, 1.0));
345
0
    out.line_to(a * Point::new(-1.0, 1.0));
346
0
    if close {
347
0
        out.close_path();
348
0
    } else {
349
0
        out.line_to(a * Point::new(-1.0, 0.0));
350
0
    }
351
0
}
352
353
0
fn extend_reversed(out: &mut BezPath, elements: &[PathEl]) {
354
0
    for i in (1..elements.len()).rev() {
355
0
        let end = elements[i - 1].end_point().unwrap();
356
0
        match elements[i] {
357
0
            PathEl::LineTo(_) => out.line_to(end),
358
0
            PathEl::QuadTo(p1, _) => out.quad_to(p1, end),
359
0
            PathEl::CurveTo(p1, p2, _) => out.curve_to(p2, p1, end),
360
0
            _ => unreachable!(),
361
        }
362
    }
363
0
}
364
365
impl StrokeCtx {
366
    /// Append forward and backward paths to output.
367
0
    fn finish(&mut self, style: &Stroke) {
368
        // TODO: scale
369
0
        let tolerance = 1e-3;
370
0
        if self.forward_path.is_empty() {
371
0
            return;
372
0
        }
373
0
        self.output.extend(&self.forward_path);
374
0
        let back_els = self.backward_path.elements();
375
0
        let return_p = back_els[back_els.len() - 1].end_point().unwrap();
376
0
        let d = self.last_pt - return_p;
377
0
        match style.end_cap {
378
0
            Cap::Butt => self.output.line_to(return_p),
379
0
            Cap::Round => round_cap(&mut self.output, tolerance, self.last_pt, d),
380
0
            Cap::Square => square_cap(&mut self.output, false, self.last_pt, d),
381
        }
382
0
        extend_reversed(&mut self.output, back_els);
383
0
        match style.start_cap {
384
0
            Cap::Butt => self.output.close_path(),
385
0
            Cap::Round => round_cap(&mut self.output, tolerance, self.start_pt, self.start_norm),
386
0
            Cap::Square => square_cap(&mut self.output, true, self.start_pt, self.start_norm),
387
        }
388
389
0
        self.forward_path.truncate(0);
390
0
        self.backward_path.truncate(0);
391
0
    }
392
393
    /// Finish a closed path
394
0
    fn finish_closed(&mut self, style: &Stroke) {
395
0
        if self.forward_path.is_empty() {
396
0
            return;
397
0
        }
398
0
        self.do_join(style, self.start_tan);
399
0
        self.output.extend(&self.forward_path);
400
0
        self.output.close_path();
401
0
        let back_els = self.backward_path.elements();
402
0
        let last_pt = back_els[back_els.len() - 1].end_point().unwrap();
403
0
        self.output.move_to(last_pt);
404
0
        extend_reversed(&mut self.output, back_els);
405
0
        self.output.close_path();
406
0
        self.forward_path.truncate(0);
407
0
        self.backward_path.truncate(0);
408
0
    }
409
410
0
    fn do_join(&mut self, style: &Stroke, tan0: Vec2) {
411
        // TODO: scale
412
0
        let tolerance = 1e-3;
413
0
        let scale = 0.5 * style.width / tan0.hypot();
414
0
        let norm = scale * Vec2::new(-tan0.y, tan0.x);
415
0
        let p0 = self.last_pt;
416
0
        if self.forward_path.elements().is_empty() {
417
0
            self.forward_path.move_to(p0 - norm);
418
0
            self.backward_path.move_to(p0 + norm);
419
0
            self.start_tan = tan0;
420
0
            self.start_norm = norm;
421
0
        } else {
422
0
            let ab = self.last_tan;
423
0
            let cd = tan0;
424
0
            let cross = ab.cross(cd);
425
0
            let dot = ab.dot(cd);
426
0
            let hypot = cross.hypot(dot);
427
            // possible TODO: a minor speedup could be squaring both sides
428
0
            if dot <= 0.0 || cross.abs() >= hypot * self.join_thresh {
429
0
                match style.join {
430
0
                    Join::Bevel => {
431
0
                        self.forward_path.line_to(p0 - norm);
432
0
                        self.backward_path.line_to(p0 + norm);
433
0
                    }
434
                    Join::Miter => {
435
0
                        if 2.0 * hypot < (hypot + dot) * style.miter_limit.powi(2) {
436
                            // TODO: maybe better to store last_norm or derive from path?
437
0
                            let last_scale = 0.5 * style.width / ab.hypot();
438
0
                            let last_norm = last_scale * Vec2::new(-ab.y, ab.x);
439
0
                            if cross > 0.0 {
440
0
                                let fp_last = p0 - last_norm;
441
0
                                let fp_this = p0 - norm;
442
0
                                let h = ab.cross(fp_this - fp_last) / cross;
443
0
                                let miter_pt = fp_this - cd * h;
444
0
                                self.forward_path.line_to(miter_pt);
445
0
                                self.backward_path.line_to(p0);
446
0
                            } else if cross < 0.0 {
447
0
                                let fp_last = p0 + last_norm;
448
0
                                let fp_this = p0 + norm;
449
0
                                let h = ab.cross(fp_this - fp_last) / cross;
450
0
                                let miter_pt = fp_this - cd * h;
451
0
                                self.backward_path.line_to(miter_pt);
452
0
                                self.forward_path.line_to(p0);
453
0
                            }
454
0
                        }
455
0
                        self.forward_path.line_to(p0 - norm);
456
0
                        self.backward_path.line_to(p0 + norm);
457
                    }
458
                    Join::Round => {
459
0
                        let angle = cross.atan2(dot);
460
0
                        if angle > 0.0 {
461
0
                            self.backward_path.line_to(p0 + norm);
462
0
                            round_join(&mut self.forward_path, tolerance, p0, norm, angle);
463
0
                        } else {
464
0
                            self.forward_path.line_to(p0 - norm);
465
0
                            round_join_rev(&mut self.backward_path, tolerance, p0, -norm, -angle);
466
0
                        }
467
                    }
468
                }
469
0
            }
470
        }
471
0
    }
472
473
0
    fn do_line(&mut self, style: &Stroke, tangent: Vec2, p1: Point) {
474
0
        let scale = 0.5 * style.width / tangent.hypot();
475
0
        let norm = scale * Vec2::new(-tangent.y, tangent.x);
476
0
        self.forward_path.line_to(p1 - norm);
477
0
        self.backward_path.line_to(p1 + norm);
478
0
        self.last_pt = p1;
479
0
    }
480
481
0
    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
0
        let chord = c.p3 - c.p0;
487
0
        let mut chord_ref = chord;
488
0
        let mut chord_ref_hypot2 = chord_ref.hypot2();
489
0
        let d01 = c.p1 - c.p0;
490
0
        if d01.hypot2() > chord_ref_hypot2 {
491
0
            chord_ref = d01;
492
0
            chord_ref_hypot2 = chord_ref.hypot2();
493
0
        }
494
0
        let d23 = c.p3 - c.p2;
495
0
        if d23.hypot2() > chord_ref_hypot2 {
496
0
            chord_ref = d23;
497
0
            chord_ref_hypot2 = chord_ref.hypot2();
498
0
        }
499
        // Project Bézier onto chord
500
0
        let p0 = c.p0.to_vec2().dot(chord_ref);
501
0
        let p1 = c.p1.to_vec2().dot(chord_ref);
502
0
        let p2 = c.p2.to_vec2().dot(chord_ref);
503
0
        let p3 = c.p3.to_vec2().dot(chord_ref);
504
        const ENDPOINT_D: f64 = 0.01;
505
0
        if p3 <= p0
506
0
            || p1 > p2
507
0
            || p1 < p0 + ENDPOINT_D * (p3 - p0)
508
0
            || p2 > p3 - ENDPOINT_D * (p3 - p0)
509
        {
510
            // potentially a cusp inside
511
0
            let x01 = d01.cross(chord_ref);
512
0
            let x23 = d23.cross(chord_ref);
513
0
            let x03 = chord.cross(chord_ref);
514
0
            let thresh = tolerance.powi(2) * chord_ref_hypot2;
515
0
            if x01 * x01 < thresh && x23 * x23 < thresh && x03 * x03 < thresh {
516
                // control points are nearly co-linear
517
0
                let midpoint = c.p0.midpoint(c.p3);
518
                // Mapping back from projection of reference chord
519
0
                let ref_vec = chord_ref / chord_ref_hypot2;
520
0
                let ref_pt = midpoint - 0.5 * (p0 + p3) * ref_vec;
521
0
                self.do_linear(style, c, [p0, p1, p2, p3], ref_pt, ref_vec);
522
0
                return;
523
0
            }
524
0
        }
525
526
0
        crate::offset::offset_cubic(c, -0.5 * style.width, tolerance, &mut self.result_path);
527
0
        self.forward_path.extend(self.result_path.iter().skip(1));
528
0
        crate::offset::offset_cubic(c, 0.5 * style.width, tolerance, &mut self.result_path);
529
0
        self.backward_path.extend(self.result_path.iter().skip(1));
530
0
        self.last_pt = c.p3;
531
0
    }
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
0
    fn do_linear(
539
0
        &mut self,
540
0
        style: &Stroke,
541
0
        c: CubicBez,
542
0
        p: [f64; 4],
543
0
        ref_pt: Point,
544
0
        ref_vec: Vec2,
545
0
    ) {
546
        // Always do round join, to model cusp as limit of finite curvature (see Nehab).
547
0
        let style = Stroke::new(style.width).with_join(Join::Round);
548
        // Tangents of endpoints (for connecting to joins)
549
0
        let (tan0, tan1) = PathSeg::Cubic(c).tangents();
550
0
        self.last_tan = tan0;
551
        // find cusps
552
0
        let c0 = p[1] - p[0];
553
0
        let c1 = 2.0 * p[2] - 4.0 * p[1] + 2.0 * p[0];
554
0
        let c2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0];
555
0
        let roots = solve_quadratic(c0, c1, c2);
556
        // discard cusps right at endpoints
557
        const EPSILON: f64 = 1e-6;
558
0
        for t in roots {
559
0
            if t > EPSILON && t < 1.0 - EPSILON {
560
0
                let mt = 1.0 - t;
561
0
                let z = mt * (mt * mt * p[0] + 3.0 * t * (mt * p[1] + t * p[2])) + t * t * t * p[3];
562
0
                let p = ref_pt + z * ref_vec;
563
0
                let tan = p - self.last_pt;
564
0
                self.do_join(&style, tan);
565
0
                self.do_line(&style, tan, p);
566
0
                self.last_tan = tan;
567
0
            }
568
        }
569
0
        let tan = c.p3 - self.last_pt;
570
0
        self.do_join(&style, tan);
571
0
        self.do_line(&style, tan, c.p3);
572
0
        self.last_tan = tan;
573
0
        self.do_join(&style, tan1);
574
0
    }
575
}
576
577
/// An implementation of dashing as an iterator-to-iterator transformation.
578
struct 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)]
600
enum DashState {
601
    NeedInput,
602
    ToStash,
603
    Working,
604
    FromStash,
605
}
606
607
impl<T: Iterator<Item = PathEl>> Iterator for DashIterator<'_, T> {
608
    type Item = PathEl;
609
610
0
    fn next(&mut self) -> Option<PathEl> {
611
        loop {
612
0
            match self.state {
613
                DashState::NeedInput => {
614
0
                    if self.input_done {
615
0
                        return None;
616
0
                    }
617
0
                    self.get_input();
618
0
                    if self.input_done {
619
0
                        return None;
620
0
                    }
621
0
                    self.state = DashState::ToStash;
622
                }
623
                DashState::ToStash => {
624
0
                    if let Some(el) = self.step() {
625
0
                        self.stash.push(el);
626
0
                    }
627
                }
628
                DashState::Working => {
629
0
                    if let Some(el) = self.step() {
630
0
                        return Some(el);
631
0
                    }
632
                }
633
                DashState::FromStash => {
634
0
                    if let Some(el) = self.stash.get(self.stash_ix) {
635
0
                        self.stash_ix += 1;
636
0
                        return Some(*el);
637
                    } else {
638
0
                        self.stash.clear();
639
0
                        self.stash_ix = 0;
640
0
                        if self.input_done {
641
0
                            return None;
642
0
                        }
643
0
                        if self.closepath_pending {
644
0
                            self.closepath_pending = false;
645
0
                            self.state = DashState::NeedInput;
646
0
                        } else {
647
0
                            self.state = DashState::ToStash;
648
0
                        }
649
                    }
650
                }
651
            }
652
        }
653
0
    }
654
}
655
656
0
fn seg_to_el(el: &PathSeg) -> PathEl {
657
0
    match el {
658
0
        PathSeg::Line(l) => PathEl::LineTo(l.p1),
659
0
        PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2),
660
0
        PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3),
661
    }
662
0
}
663
664
const 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.
679
0
pub fn dash<'a>(
680
0
    inner: impl Iterator<Item = PathEl> + 'a,
681
0
    dash_offset: f64,
682
0
    dashes: &'a [f64],
683
0
) -> impl Iterator<Item = PathEl> + 'a {
684
    // ensure that offset is positive and minimal by normalization using period
685
0
    let period = dashes.iter().sum();
686
0
    let dash_offset = dash_offset.rem_euclid(period);
687
688
0
    let mut dash_ix = 0;
689
0
    let mut dash_remaining = dashes[dash_ix] - dash_offset;
690
0
    let mut is_active = true;
691
    // Find place in dashes array for initial offset.
692
0
    while dash_remaining < 0.0 {
693
0
        dash_ix = (dash_ix + 1) % dashes.len();
694
0
        dash_remaining += dashes[dash_ix];
695
0
        is_active = !is_active;
696
0
    }
697
0
    DashIterator {
698
0
        inner,
699
0
        input_done: false,
700
0
        closepath_pending: false,
701
0
        dashes,
702
0
        dash_ix,
703
0
        init_dash_ix: dash_ix,
704
0
        init_dash_remaining: dash_remaining,
705
0
        init_is_active: is_active,
706
0
        is_active,
707
0
        state: DashState::NeedInput,
708
0
        current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)),
709
0
        t: 0.0,
710
0
        dash_remaining,
711
0
        seg_remaining: 0.0,
712
0
        start_pt: Point::ORIGIN,
713
0
        last_pt: Point::ORIGIN,
714
0
        stash: Vec::new(),
715
0
        stash_ix: 0,
716
0
    }
717
0
}
718
719
impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> {
720
0
    fn get_input(&mut self) {
721
        loop {
722
0
            if self.closepath_pending {
723
0
                self.handle_closepath();
724
0
                break;
725
0
            }
726
0
            let Some(next_el) = self.inner.next() else {
727
0
                self.input_done = true;
728
0
                self.state = DashState::FromStash;
729
0
                return;
730
            };
731
0
            let p0 = self.last_pt;
732
0
            match next_el {
733
0
                PathEl::MoveTo(p) => {
734
0
                    if !self.stash.is_empty() {
735
0
                        self.state = DashState::FromStash;
736
0
                    }
737
0
                    self.start_pt = p;
738
0
                    self.last_pt = p;
739
0
                    self.reset_phase();
740
0
                    continue;
741
                }
742
0
                PathEl::LineTo(p1) => {
743
0
                    let l = Line::new(p0, p1);
744
0
                    self.seg_remaining = l.arclen(DASH_ACCURACY);
745
0
                    self.current_seg = PathSeg::Line(l);
746
0
                    self.last_pt = p1;
747
0
                }
748
0
                PathEl::QuadTo(p1, p2) => {
749
0
                    let q = QuadBez::new(p0, p1, p2);
750
0
                    self.seg_remaining = q.arclen(DASH_ACCURACY);
751
0
                    self.current_seg = PathSeg::Quad(q);
752
0
                    self.last_pt = p2;
753
0
                }
754
0
                PathEl::CurveTo(p1, p2, p3) => {
755
0
                    let c = CubicBez::new(p0, p1, p2, p3);
756
0
                    self.seg_remaining = c.arclen(DASH_ACCURACY);
757
0
                    self.current_seg = PathSeg::Cubic(c);
758
0
                    self.last_pt = p3;
759
0
                }
760
                PathEl::ClosePath => {
761
0
                    self.closepath_pending = true;
762
0
                    if p0 != self.start_pt {
763
0
                        let l = Line::new(p0, self.start_pt);
764
0
                        self.seg_remaining = l.arclen(DASH_ACCURACY);
765
0
                        self.current_seg = PathSeg::Line(l);
766
0
                        self.last_pt = self.start_pt;
767
0
                    } else {
768
0
                        self.handle_closepath();
769
0
                    }
770
                }
771
            }
772
0
            break;
773
        }
774
0
        self.t = 0.0;
775
0
    }
776
777
    /// Move arc length forward to next event.
778
0
    fn step(&mut self) -> Option<PathEl> {
779
0
        let mut result = None;
780
0
        if self.state == DashState::ToStash && self.stash.is_empty() {
781
0
            if self.is_active {
782
0
                result = Some(PathEl::MoveTo(self.current_seg.start()));
783
0
            } else {
784
0
                self.state = DashState::Working;
785
0
            }
786
0
        } else if self.dash_remaining < self.seg_remaining {
787
            // next transition is a dash transition
788
0
            let seg = self.current_seg.subsegment(self.t..1.0);
789
0
            let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY);
790
0
            if self.is_active {
791
0
                let subseg = seg.subsegment(0.0..t1);
792
0
                result = Some(seg_to_el(&subseg));
793
0
                self.state = DashState::Working;
794
0
            } else {
795
0
                let p = seg.eval(t1);
796
0
                result = Some(PathEl::MoveTo(p));
797
0
            }
798
0
            self.is_active = !self.is_active;
799
0
            self.t += t1 * (1.0 - self.t);
800
0
            self.seg_remaining -= self.dash_remaining;
801
0
            self.dash_ix += 1;
802
0
            if self.dash_ix == self.dashes.len() {
803
0
                self.dash_ix = 0;
804
0
            }
805
0
            self.dash_remaining = self.dashes[self.dash_ix];
806
        } else {
807
0
            if self.is_active {
808
0
                let seg = self.current_seg.subsegment(self.t..1.0);
809
0
                result = Some(seg_to_el(&seg));
810
0
            }
811
0
            self.dash_remaining -= self.seg_remaining;
812
0
            self.get_input();
813
        }
814
0
        result
815
0
    }
816
817
0
    fn handle_closepath(&mut self) {
818
0
        if self.state == DashState::ToStash {
819
0
            // Have looped back without breaking a dash, just play it back
820
0
            self.stash.push(PathEl::ClosePath);
821
0
        } else if self.is_active {
822
0
            // connect with path in stash, skip MoveTo.
823
0
            self.stash_ix = 1;
824
0
        }
825
0
        self.state = DashState::FromStash;
826
0
        self.reset_phase();
827
0
    }
828
829
0
    fn reset_phase(&mut self) {
830
0
        self.dash_ix = self.init_dash_ix;
831
0
        self.dash_remaining = self.init_dash_remaining;
832
0
        self.is_active = self.init_is_active;
833
0
    }
834
}
835
836
#[cfg(test)]
837
mod 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
}