1use 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#[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 Bevel,
25 Miter,
27 Round,
29}
30
31#[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 Butt,
38 Square,
40 Round,
42}
43
44#[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 pub width: f64,
51 pub join: Join,
53 pub miter_limit: f64,
55 pub start_cap: Cap,
57 pub end_cap: Cap,
59 pub dash_pattern: Dashes,
61 pub dash_offset: f64,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct StrokeOpts {
68 opt_level: StrokeOptLevel,
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub enum StrokeOptLevel {
74 Subdivide,
76 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 pub fn new(width: f64) -> Self {
104 Self {
105 width,
106 ..Stroke::default()
107 }
108 }
109
110 pub fn with_join(mut self, join: Join) -> Self {
112 self.join = join;
113 self
114 }
115
116 pub fn with_miter_limit(mut self, limit: f64) -> Self {
118 self.miter_limit = limit;
119 self
120 }
121
122 pub fn with_start_cap(mut self, cap: Cap) -> Self {
124 self.start_cap = cap;
125 self
126 }
127
128 pub fn with_end_cap(mut self, cap: Cap) -> Self {
130 self.end_cap = cap;
131 self
132 }
133
134 pub fn with_caps(mut self, cap: Cap) -> Self {
136 self.start_cap = cap;
137 self.end_cap = cap;
138 self
139 }
140
141 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 pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self {
158 self.opt_level = opt_level;
159 self
160 }
161}
162
163pub type Dashes = SmallVec<[f64; 4]>;
165
166#[derive(Default)]
168struct StrokeCtx {
169 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 join_thresh: f64,
183}
184
185pub 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
213fn 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 fn finish(&mut self, style: &Stroke) {
322 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 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 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 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 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 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 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 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 let midpoint = c.p0.midpoint(c.p3);
470 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 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 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 let style = Stroke::new(style.width).with_join(Join::Round);
507 let (tan0, tan1) = PathSeg::Cubic(c).tangents();
509 self.last_tan = tan0;
510 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 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#[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
626pub 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
647fn dash_impl<T: Iterator<Item = PathEl>>(
649 inner: T,
650 dash_offset: f64,
651 dashes: &[f64],
652) -> DashIterator<'_, T> {
653 let period = dashes.iter().sum();
655 let dash_offset = dash_offset.rem_euclid(period);
656
657 let mut dash_ix = 0;
658 let mut dash_remaining = dashes[dash_ix] - dash_offset;
659 let mut is_active = true;
660 while dash_remaining < 0.0 {
662 dash_ix = (dash_ix + 1) % dashes.len();
663 dash_remaining += dashes[dash_ix];
664 is_active = !is_active;
665 }
666 DashIterator {
667 inner,
668 input_done: false,
669 closepath_pending: false,
670 dashes,
671 dash_ix,
672 init_dash_ix: dash_ix,
673 init_dash_remaining: dash_remaining,
674 init_is_active: is_active,
675 is_active,
676 state: DashState::NeedInput,
677 current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)),
678 t: 0.0,
679 dash_remaining,
680 seg_remaining: 0.0,
681 start_pt: Point::ORIGIN,
682 last_pt: Point::ORIGIN,
683 stash: Vec::new(),
684 stash_ix: 0,
685 }
686}
687
688impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> {
689 #[doc(hidden)]
690 #[deprecated(since = "0.10.4", note = "use dash() instead")]
691 pub fn new(inner: T, dash_offset: f64, dashes: &'a [f64]) -> Self {
692 dash_impl(inner, dash_offset, dashes)
693 }
694
695 fn get_input(&mut self) {
696 loop {
697 if self.closepath_pending {
698 self.handle_closepath();
699 break;
700 }
701 let Some(next_el) = self.inner.next() else {
702 self.input_done = true;
703 self.state = DashState::FromStash;
704 return;
705 };
706 let p0 = self.last_pt;
707 match next_el {
708 PathEl::MoveTo(p) => {
709 if !self.stash.is_empty() {
710 self.state = DashState::FromStash;
711 }
712 self.start_pt = p;
713 self.last_pt = p;
714 self.reset_phase();
715 continue;
716 }
717 PathEl::LineTo(p1) => {
718 let l = Line::new(p0, p1);
719 self.seg_remaining = l.arclen(DASH_ACCURACY);
720 self.current_seg = PathSeg::Line(l);
721 self.last_pt = p1;
722 }
723 PathEl::QuadTo(p1, p2) => {
724 let q = QuadBez::new(p0, p1, p2);
725 self.seg_remaining = q.arclen(DASH_ACCURACY);
726 self.current_seg = PathSeg::Quad(q);
727 self.last_pt = p2;
728 }
729 PathEl::CurveTo(p1, p2, p3) => {
730 let c = CubicBez::new(p0, p1, p2, p3);
731 self.seg_remaining = c.arclen(DASH_ACCURACY);
732 self.current_seg = PathSeg::Cubic(c);
733 self.last_pt = p3;
734 }
735 PathEl::ClosePath => {
736 self.closepath_pending = true;
737 if p0 != self.start_pt {
738 let l = Line::new(p0, self.start_pt);
739 self.seg_remaining = l.arclen(DASH_ACCURACY);
740 self.current_seg = PathSeg::Line(l);
741 self.last_pt = self.start_pt;
742 } else {
743 self.handle_closepath();
744 }
745 }
746 }
747 break;
748 }
749 self.t = 0.0;
750 }
751
752 fn step(&mut self) -> Option<PathEl> {
754 let mut result = None;
755 if self.state == DashState::ToStash && self.stash.is_empty() {
756 if self.is_active {
757 result = Some(PathEl::MoveTo(self.current_seg.start()));
758 } else {
759 self.state = DashState::Working;
760 }
761 } else if self.dash_remaining < self.seg_remaining {
762 let seg = self.current_seg.subsegment(self.t..1.0);
764 let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY);
765 if self.is_active {
766 let subseg = seg.subsegment(0.0..t1);
767 result = Some(seg_to_el(&subseg));
768 self.state = DashState::Working;
769 } else {
770 let p = seg.eval(t1);
771 result = Some(PathEl::MoveTo(p));
772 }
773 self.is_active = !self.is_active;
774 self.t += t1 * (1.0 - self.t);
775 self.seg_remaining -= self.dash_remaining;
776 self.dash_ix += 1;
777 if self.dash_ix == self.dashes.len() {
778 self.dash_ix = 0;
779 }
780 self.dash_remaining = self.dashes[self.dash_ix];
781 } else {
782 if self.is_active {
783 let seg = self.current_seg.subsegment(self.t..1.0);
784 result = Some(seg_to_el(&seg));
785 }
786 self.dash_remaining -= self.seg_remaining;
787 self.get_input();
788 }
789 result
790 }
791
792 fn handle_closepath(&mut self) {
793 if self.state == DashState::ToStash {
794 self.stash.push(PathEl::ClosePath);
796 } else if self.is_active {
797 self.stash_ix = 1;
799 }
800 self.state = DashState::FromStash;
801 self.reset_phase();
802 }
803
804 fn reset_phase(&mut self) {
805 self.dash_ix = self.init_dash_ix;
806 self.dash_remaining = self.init_dash_remaining;
807 self.is_active = self.init_is_active;
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use crate::{
814 dash, segments, stroke, Cap::Butt, CubicBez, Join::Miter, Line, PathSeg, Shape, Stroke,
815 StrokeOpts,
816 };
817
818 #[test]
820 fn pathological_stroke() {
821 let curve = CubicBez::new(
822 (602.469, 286.585),
823 (641.975, 286.585),
824 (562.963, 286.585),
825 (562.963, 286.585),
826 );
827 let path = curve.into_path(0.1);
828 let stroke_style = Stroke::new(1.);
829 let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001);
830 assert!(stroked.is_finite());
831 }
832
833 #[test]
835 fn broken_strokes() {
836 let broken_cubics = [
837 [
838 (465.24423, 107.11105),
839 (475.50754, 107.11105),
840 (475.50754, 107.11105),
841 (475.50754, 107.11105),
842 ],
843 [(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], [(0., 0.), (0., -10.), (0., -10.), (0., 10.)], [(10., 0.), (0., 0.), (20., 0.), (10., 0.)], [(39., -39.), (40., -40.), (40., -40.), (0., 0.)], [(40., 40.), (0., 0.), (200., 200.), (0., 0.)], [(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], [
851 (400.75, 100.05),
852 (400.75, 100.05),
853 (100.05, 300.95),
854 (100.05, 300.95),
855 ],
856 [(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], ];
859 let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter);
860 for cubic in &broken_cubics {
861 let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1);
862 let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001);
863 assert!(stroked.is_finite());
864 }
865 }
866
867 #[test]
868 fn dash_sequence() {
869 let shape = Line::new((0.0, 0.0), (21.0, 0.0));
870 let dashes = [1., 5., 2., 5.];
871 let expansion = [
872 PathSeg::Line(Line::new((6., 0.), (8., 0.))),
873 PathSeg::Line(Line::new((13., 0.), (14., 0.))),
874 PathSeg::Line(Line::new((19., 0.), (21., 0.))),
875 PathSeg::Line(Line::new((0., 0.), (1., 0.))),
876 ];
877 let iter = segments(dash(shape.path_elements(0.), 0., &dashes));
878 assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
879 }
880
881 #[test]
882 fn dash_sequence_offset() {
883 let shape = Line::new((0.0, 0.0), (21.0, 0.0));
887 let dashes = [1., 5., 2., 5.];
888 let expansion = [
889 PathSeg::Line(Line::new((3., 0.), (5., 0.))),
890 PathSeg::Line(Line::new((10., 0.), (11., 0.))),
891 PathSeg::Line(Line::new((16., 0.), (18., 0.))),
892 ];
893 let iter = segments(dash(shape.path_elements(0.), 3., &dashes));
894 assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
895 }
896
897 #[test]
898 fn dash_negative_offset() {
899 let shape = Line::new((0.0, 0.0), (28.0, 0.0));
900 let dashes = [4., 2.];
901 let pos = segments(dash(shape.path_elements(0.), 60., &dashes)).collect::<Vec<PathSeg>>();
902 let neg = segments(dash(shape.path_elements(0.), -60., &dashes)).collect::<Vec<PathSeg>>();
903 assert_eq!(neg, pos);
904 }
905}