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 mut dash_ix = 0;
654 let mut dash_remaining = dashes[dash_ix] - dash_offset;
655 let mut is_active = true;
656 while dash_remaining < 0.0 {
658 dash_ix = (dash_ix + 1) % dashes.len();
659 dash_remaining += dashes[dash_ix];
660 is_active = !is_active;
661 }
662 DashIterator {
663 inner,
664 input_done: false,
665 closepath_pending: false,
666 dashes,
667 dash_ix,
668 init_dash_ix: dash_ix,
669 init_dash_remaining: dash_remaining,
670 init_is_active: is_active,
671 is_active,
672 state: DashState::NeedInput,
673 current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)),
674 t: 0.0,
675 dash_remaining,
676 seg_remaining: 0.0,
677 start_pt: Point::ORIGIN,
678 last_pt: Point::ORIGIN,
679 stash: Vec::new(),
680 stash_ix: 0,
681 }
682}
683
684impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> {
685 #[doc(hidden)]
686 #[deprecated(since = "0.10.4", note = "use dash() instead")]
687 pub fn new(inner: T, dash_offset: f64, dashes: &'a [f64]) -> Self {
688 dash_impl(inner, dash_offset, dashes)
689 }
690
691 fn get_input(&mut self) {
692 loop {
693 if self.closepath_pending {
694 self.handle_closepath();
695 break;
696 }
697 let Some(next_el) = self.inner.next() else {
698 self.input_done = true;
699 self.state = DashState::FromStash;
700 return;
701 };
702 let p0 = self.last_pt;
703 match next_el {
704 PathEl::MoveTo(p) => {
705 if !self.stash.is_empty() {
706 self.state = DashState::FromStash;
707 }
708 self.start_pt = p;
709 self.last_pt = p;
710 self.reset_phase();
711 continue;
712 }
713 PathEl::LineTo(p1) => {
714 let l = Line::new(p0, p1);
715 self.seg_remaining = l.arclen(DASH_ACCURACY);
716 self.current_seg = PathSeg::Line(l);
717 self.last_pt = p1;
718 }
719 PathEl::QuadTo(p1, p2) => {
720 let q = QuadBez::new(p0, p1, p2);
721 self.seg_remaining = q.arclen(DASH_ACCURACY);
722 self.current_seg = PathSeg::Quad(q);
723 self.last_pt = p2;
724 }
725 PathEl::CurveTo(p1, p2, p3) => {
726 let c = CubicBez::new(p0, p1, p2, p3);
727 self.seg_remaining = c.arclen(DASH_ACCURACY);
728 self.current_seg = PathSeg::Cubic(c);
729 self.last_pt = p3;
730 }
731 PathEl::ClosePath => {
732 self.closepath_pending = true;
733 if p0 != self.start_pt {
734 let l = Line::new(p0, self.start_pt);
735 self.seg_remaining = l.arclen(DASH_ACCURACY);
736 self.current_seg = PathSeg::Line(l);
737 self.last_pt = self.start_pt;
738 } else {
739 self.handle_closepath();
740 }
741 }
742 }
743 break;
744 }
745 self.t = 0.0;
746 }
747
748 fn step(&mut self) -> Option<PathEl> {
750 let mut result = None;
751 if self.state == DashState::ToStash && self.stash.is_empty() {
752 if self.is_active {
753 result = Some(PathEl::MoveTo(self.current_seg.start()));
754 } else {
755 self.state = DashState::Working;
756 }
757 } else if self.dash_remaining < self.seg_remaining {
758 let seg = self.current_seg.subsegment(self.t..1.0);
760 let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY);
761 if self.is_active {
762 let subseg = seg.subsegment(0.0..t1);
763 result = Some(seg_to_el(&subseg));
764 self.state = DashState::Working;
765 } else {
766 let p = seg.eval(t1);
767 result = Some(PathEl::MoveTo(p));
768 }
769 self.is_active = !self.is_active;
770 self.t += t1 * (1.0 - self.t);
771 self.seg_remaining -= self.dash_remaining;
772 self.dash_ix += 1;
773 if self.dash_ix == self.dashes.len() {
774 self.dash_ix = 0;
775 }
776 self.dash_remaining = self.dashes[self.dash_ix];
777 } else {
778 if self.is_active {
779 let seg = self.current_seg.subsegment(self.t..1.0);
780 result = Some(seg_to_el(&seg));
781 }
782 self.dash_remaining -= self.seg_remaining;
783 self.get_input();
784 }
785 result
786 }
787
788 fn handle_closepath(&mut self) {
789 if self.state == DashState::ToStash {
790 self.stash.push(PathEl::ClosePath);
792 } else if self.is_active {
793 self.stash_ix = 1;
795 }
796 self.state = DashState::FromStash;
797 self.reset_phase();
798 }
799
800 fn reset_phase(&mut self) {
801 self.dash_ix = self.init_dash_ix;
802 self.dash_remaining = self.init_dash_remaining;
803 self.is_active = self.init_is_active;
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use crate::{
810 dash, segments, stroke, Cap::Butt, CubicBez, Join::Miter, Line, PathSeg, Shape, Stroke,
811 StrokeOpts,
812 };
813
814 #[test]
816 fn pathological_stroke() {
817 let curve = CubicBez::new(
818 (602.469, 286.585),
819 (641.975, 286.585),
820 (562.963, 286.585),
821 (562.963, 286.585),
822 );
823 let path = curve.into_path(0.1);
824 let stroke_style = Stroke::new(1.);
825 let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001);
826 assert!(stroked.is_finite());
827 }
828
829 #[test]
831 fn broken_strokes() {
832 let broken_cubics = [
833 [
834 (465.24423, 107.11105),
835 (475.50754, 107.11105),
836 (475.50754, 107.11105),
837 (475.50754, 107.11105),
838 ],
839 [(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], [(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.)], [
847 (400.75, 100.05),
848 (400.75, 100.05),
849 (100.05, 300.95),
850 (100.05, 300.95),
851 ],
852 [(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], ];
855 let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter);
856 for cubic in &broken_cubics {
857 let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1);
858 let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001);
859 assert!(stroked.is_finite());
860 }
861 }
862
863 #[test]
864 fn dash_sequence() {
865 let shape = Line::new((0.0, 0.0), (21.0, 0.0));
866 let dashes = [1., 5., 2., 5.];
867 let expansion = [
868 PathSeg::Line(Line::new((6., 0.), (8., 0.))),
869 PathSeg::Line(Line::new((13., 0.), (14., 0.))),
870 PathSeg::Line(Line::new((19., 0.), (21., 0.))),
871 PathSeg::Line(Line::new((0., 0.), (1., 0.))),
872 ];
873 let iter = segments(dash(shape.path_elements(0.), 0., &dashes));
874 assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
875 }
876
877 #[test]
878 fn dash_sequence_offset() {
879 let shape = Line::new((0.0, 0.0), (21.0, 0.0));
883 let dashes = [1., 5., 2., 5.];
884 let expansion = [
885 PathSeg::Line(Line::new((3., 0.), (5., 0.))),
886 PathSeg::Line(Line::new((10., 0.), (11., 0.))),
887 PathSeg::Line(Line::new((16., 0.), (18., 0.))),
888 ];
889 let iter = segments(dash(shape.path_elements(0.), 3., &dashes));
890 assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
891 }
892}