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, Affine, Arc, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen,
15 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)]
79pub enum StrokeOptLevel {
80 Subdivide,
82 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 pub fn new(width: f64) -> Self {
110 Self {
111 width,
112 ..Stroke::default()
113 }
114 }
115
116 pub fn with_join(mut self, join: Join) -> Self {
118 self.join = join;
119 self
120 }
121
122 pub fn with_miter_limit(mut self, limit: f64) -> Self {
124 self.miter_limit = limit;
125 self
126 }
127
128 pub fn with_start_cap(mut self, cap: Cap) -> Self {
130 self.start_cap = cap;
131 self
132 }
133
134 pub fn with_end_cap(mut self, cap: Cap) -> Self {
136 self.end_cap = cap;
137 self
138 }
139
140 pub fn with_caps(mut self, cap: Cap) -> Self {
142 self.start_cap = cap;
143 self.end_cap = cap;
144 self
145 }
146
147 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 pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self {
164 self.opt_level = opt_level;
165 self
166 }
167}
168
169pub type Dashes = SmallVec<[f64; 4]>;
171
172#[derive(Default, Debug)]
176pub struct StrokeCtx {
177 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 join_thresh: f64,
192}
193
194impl StrokeCtx {
195 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
215pub 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
244pub 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
268fn 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 fn finish(&mut self, style: &Stroke) {
368 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 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 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 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 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 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 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 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 let midpoint = c.p0.midpoint(c.p3);
518 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 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 let style = Stroke::new(style.width).with_join(Join::Round);
548 let (tan0, tan1) = PathSeg::Cubic(c).tangents();
550 self.last_tan = tan0;
551 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 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
577struct 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
666pub 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 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 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 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 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 self.stash.push(PathEl::ClosePath);
821 } else if self.is_active {
822 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 #[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 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 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 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]
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)], [(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.)], [
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.)], [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], ];
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 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}