1use crate::{Contour, Point};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum Cap {
13 #[default]
14 Butt,
15 Round,
16 Square,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum Join {
22 #[default]
23 Miter,
24 Round,
25 Bevel,
26}
27
28#[derive(Clone, Debug, PartialEq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct Dash {
32 pub intervals: Vec<f32>,
33 pub phase: f32,
34}
35
36#[derive(Clone, Debug, PartialEq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize))]
38pub struct Stroke {
39 pub width: f32,
40 pub cap: Cap,
41 pub join: Join,
42 pub miter_limit: f32,
44 pub dash: Option<Dash>,
45}
46
47impl Stroke {
48 pub fn new(width: f32) -> Self {
49 Self {
50 width,
51 cap: Cap::default(),
52 join: Join::default(),
53 miter_limit: 4.0,
54 dash: None,
55 }
56 }
57}
58
59pub fn stroke_strip(contours: &[Contour], stroke: &Stroke, tolerance: f32) -> Vec<f32> {
63 let half = stroke.width * 0.5;
64 if half <= 0.0 {
65 return Vec::new();
66 }
67 let mut strip = Strip::default();
68 for contour in contours {
69 let mut pts = dedup(&contour.points);
70 if contour.closed && pts.len() >= 2 && distance(pts[0], *pts.last().unwrap()) < 1e-4 {
73 pts.pop();
74 }
75 match pts.len() {
76 0 => {}
77 1 if contour.has_segments => lone_point(&mut strip, pts[0], stroke, half, tolerance),
84 1 => {}
85 _ => stroke_contour(&mut strip, &pts, contour.closed, stroke, half, tolerance),
86 }
87 }
88 strip.out
89}
90
91pub fn stroke_contains(
99 contours: &[Contour],
100 stroke: &Stroke,
101 tolerance: f32,
102 point: Point,
103) -> bool {
104 let strip = stroke_strip(contours, stroke, tolerance);
105 let vertex = |index: usize| Point::new(strip[index * 2], strip[index * 2 + 1]);
106 let vertices = strip.len() / 2;
107 (2..vertices).any(|i| in_triangle(point, vertex(i - 2), vertex(i - 1), vertex(i)))
108}
109
110fn in_triangle(p: Point, a: Point, b: Point, c: Point) -> bool {
126 let area = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
127 if area == 0.0 {
128 return false;
129 }
130 let side = |from: Point, to: Point| {
131 (to.x - from.x) * (p.y - from.y) - (to.y - from.y) * (p.x - from.x)
132 };
133 let (ab, bc, ca) = (side(a, b), side(b, c), side(c, a));
134 let negative = ab < 0.0 || bc < 0.0 || ca < 0.0;
135 let positive = ab > 0.0 || bc > 0.0 || ca > 0.0;
136 !(negative && positive)
137}
138
139pub fn dash_contours(contours: &[Contour], dash: &Dash) -> Vec<Contour> {
143 let Some(dash) = normalize_dash(dash) else {
144 return contours.to_vec();
145 };
146 let mut out = Vec::new();
147 for contour in contours {
148 dash_contour(&mut out, &contour.points, &dash);
149 }
150 out
151}
152
153fn normalize_dash(dash: &Dash) -> Option<Dash> {
156 let sum: f32 = dash.intervals.iter().sum();
157 if dash.intervals.is_empty() || sum <= 0.0 || dash.intervals.iter().any(|&v| v < 0.0) {
158 return None;
159 }
160 let mut intervals = dash.intervals.clone();
161 if intervals.len() % 2 == 1 {
162 intervals.extend(dash.intervals.iter().copied());
163 }
164 Some(Dash {
165 intervals,
166 phase: dash.phase,
167 })
168}
169
170#[derive(Default)]
173struct Strip {
174 out: Vec<f32>,
175}
176
177impl Strip {
178 fn emit(&mut self, p: Point) {
179 self.out.extend_from_slice(&[p.x, p.y]);
180 }
181
182 fn stitch(&mut self, next: Point) {
184 if self.out.is_empty() {
185 self.emit(next);
186 return;
187 }
188 let last = Point::new(self.out[self.out.len() - 2], self.out[self.out.len() - 1]);
189 self.emit(last);
190 self.emit(next);
191 self.emit(next);
192 }
193}
194
195fn stroke_contour(
196 strip: &mut Strip,
197 pts: &[Point],
198 closed: bool,
199 stroke: &Stroke,
200 half: f32,
201 tolerance: f32,
202) {
203 let first_normal = normal(pts[0], pts[1], half);
204 if closed {
205 strip.stitch(add(pts[0], first_normal));
206 } else {
207 start_cap(strip, pts[0], pts[1], stroke.cap, half, tolerance);
208 strip.emit(add(pts[0], first_normal));
209 }
210 strip.emit(sub(pts[0], first_normal));
211
212 let segments = if closed { pts.len() } else { pts.len() - 1 };
213 for i in 0..segments {
214 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
215 let n = normal(a, b, half);
216 strip.emit(add(b, n));
217 strip.emit(sub(b, n));
218 let last = i + 1 == segments;
219 if !last || closed {
220 let c = pts[(i + 2) % pts.len()];
221 join(strip, b, a, c, stroke, half, tolerance);
222 let n_next = normal(b, c, half);
223 strip.emit(add(b, n_next));
224 strip.emit(sub(b, n_next));
225 }
226 }
227 if !closed {
228 end_cap(
229 strip,
230 pts[pts.len() - 2],
231 pts[pts.len() - 1],
232 stroke.cap,
233 half,
234 tolerance,
235 );
236 }
237}
238
239fn join(
242 strip: &mut Strip,
243 p: Point,
244 a: Point,
245 c: Point,
246 stroke: &Stroke,
247 half: f32,
248 tolerance: f32,
249) {
250 let d0 = direction(a, p);
251 let d1 = direction(p, c);
252 let cross = d0.x * d1.y - d0.y * d1.x;
253 if cross.abs() < 1e-6 {
254 return; }
256 let s = if cross > 0.0 { -1.0 } else { 1.0 };
259 let n0 = scale(perp(d0), half * s);
260 let n1 = scale(perp(d1), half * s);
261 let from = add(p, n0);
262 let to = add(p, n1);
263 match stroke.join {
264 Join::Bevel => fan(strip, p, &[from, to]),
265 Join::Miter => {
266 let dot = d0.x * d1.x + d0.y * d1.y;
267 let ratio = (2.0 / (1.0 + dot).max(1e-6)).sqrt();
269 if ratio > stroke.miter_limit.max(1.0) {
270 fan(strip, p, &[from, to]);
271 } else {
272 let m = Point::new(n0.x + n1.x, n0.y + n1.y);
273 let tip = add(p, scale(m, 1.0 / (1.0 + dot).max(1e-6)));
274 fan(strip, p, &[from, tip, to]);
275 }
276 }
277 Join::Round => {
278 let points = arc_points(p, n0, n1, half, tolerance);
279 fan(strip, p, &points);
280 }
281 }
282}
283
284fn fan(strip: &mut Strip, pivot: Point, rim: &[Point]) {
287 for &q in rim {
288 strip.emit(q);
289 strip.emit(pivot);
290 }
291}
292
293fn start_cap(strip: &mut Strip, p: Point, toward: Point, cap: Cap, half: f32, tolerance: f32) {
294 let d = direction(p, toward);
295 let n = scale(perp(d), half);
296 match cap {
297 Cap::Butt => strip.stitch(add(p, n)),
298 Cap::Square => {
299 let back = sub(p, scale(d, half));
300 strip.stitch(add(back, n));
301 strip.emit(sub(back, n));
302 }
303 Cap::Round => {
304 let back = scale(d, -half);
307 let mut rim = arc_points(p, scale(n, -1.0), back, half, tolerance);
308 rim.extend(arc_points(p, back, n, half, tolerance));
309 strip.stitch(p);
310 fan(strip, p, &rim);
311 }
312 }
313}
314
315fn end_cap(strip: &mut Strip, from: Point, p: Point, cap: Cap, half: f32, tolerance: f32) {
316 let d = direction(from, p);
317 let n = scale(perp(d), half);
318 match cap {
319 Cap::Butt => {}
320 Cap::Square => {
321 let out = add(p, scale(d, half));
322 strip.emit(add(out, n));
323 strip.emit(sub(out, n));
324 }
325 Cap::Round => {
326 let fwd = scale(d, half);
328 let mut rim = arc_points(p, n, fwd, half, tolerance);
329 rim.extend(arc_points(p, fwd, scale(n, -1.0), half, tolerance));
330 fan(strip, p, &rim);
331 }
332 }
333}
334
335fn lone_point(strip: &mut Strip, p: Point, stroke: &Stroke, half: f32, tolerance: f32) {
354 match stroke.cap {
355 Cap::Butt => {}
356 Cap::Round => {
357 let (r, l) = (Point::new(half, 0.0), Point::new(-half, 0.0));
359 let (dn, up) = (Point::new(0.0, half), Point::new(0.0, -half));
360 let mut rim = arc_points(p, r, dn, half, tolerance);
361 rim.extend(arc_points(p, dn, l, half, tolerance));
362 rim.extend(arc_points(p, l, up, half, tolerance));
363 rim.extend(arc_points(p, up, r, half, tolerance));
364 strip.stitch(p);
365 fan(strip, p, &rim);
366 }
367 Cap::Square => {
368 strip.stitch(Point::new(p.x - half, p.y - half));
369 strip.emit(Point::new(p.x - half, p.y + half));
370 strip.emit(Point::new(p.x + half, p.y - half));
371 strip.emit(Point::new(p.x + half, p.y + half));
372 }
373 }
374}
375
376fn arc_points(center: Point, from: Point, to: Point, radius: f32, tolerance: f32) -> Vec<Point> {
379 let a0 = from.y.atan2(from.x);
380 let mut a1 = to.y.atan2(to.x);
381 let mut sweep = a1 - a0;
382 if sweep > std::f32::consts::PI {
383 a1 -= std::f32::consts::TAU;
384 sweep = a1 - a0;
385 } else if sweep < -std::f32::consts::PI {
386 a1 += std::f32::consts::TAU;
387 sweep = a1 - a0;
388 }
389 let max_step = 2.0
390 * (1.0 - (tolerance / radius.max(1e-3)).clamp(0.0, 0.5))
391 .acos()
392 .max(0.1);
393 let steps = (sweep.abs() / max_step).ceil().max(1.0) as usize;
394 (0..=steps)
395 .map(|i| {
396 let t = a0 + sweep * (i as f32 / steps as f32);
397 Point::new(center.x + radius * t.cos(), center.y + radius * t.sin())
398 })
399 .collect()
400}
401
402fn dash_contour(out: &mut Vec<Contour>, contour: &[Point], dash: &Dash) {
405 let cycle: f32 = dash.intervals.iter().sum();
406 let (mut index, mut remaining) = interval_at(&dash.intervals, dash.phase.rem_euclid(cycle));
407 let mut on = index % 2 == 0;
408 let mut current: Vec<Point> = Vec::new();
409 if on {
410 current.push(contour[0]);
411 }
412 for pair in contour.windows(2) {
413 let (mut a, b) = (pair[0], pair[1]);
414 let mut len = distance(a, b);
415 while len > remaining {
423 let cut = lerp(a, b, remaining / len);
424 if on {
425 current.push(cut);
426 out.push(open_contour(std::mem::take(&mut current)));
427 } else {
428 current.push(cut);
429 }
430 on = !on;
431 a = cut;
432 len -= remaining;
433 index += 1;
434 remaining = dash.intervals[index % dash.intervals.len()];
435 }
436 remaining -= len;
437 if on {
438 current.push(b);
439 }
440 }
441 if on && current.len() > 1 {
442 out.push(open_contour(current));
443 }
444}
445
446fn open_contour(points: Vec<Point>) -> Contour {
450 Contour {
451 points,
452 closed: false,
453 has_segments: true,
454 }
455}
456
457fn interval_at(intervals: &[f32], offset: f32) -> (usize, f32) {
472 let mut left = offset;
473 for (i, &len) in intervals.iter().enumerate() {
474 if left < len || (len <= 0.0 && left <= 0.0) {
475 return (i, len - left);
476 }
477 left -= len;
478 }
479 (0, intervals[0])
480}
481
482fn direction(a: Point, b: Point) -> Point {
485 let (dx, dy) = (b.x - a.x, b.y - a.y);
486 let len = (dx * dx + dy * dy).sqrt().max(1e-6);
487 Point::new(dx / len, dy / len)
488}
489
490fn perp(d: Point) -> Point {
491 Point::new(-d.y, d.x)
492}
493
494fn normal(a: Point, b: Point, half: f32) -> Point {
495 scale(perp(direction(a, b)), half)
496}
497
498fn add(p: Point, v: Point) -> Point {
499 Point::new(p.x + v.x, p.y + v.y)
500}
501
502fn sub(p: Point, v: Point) -> Point {
503 Point::new(p.x - v.x, p.y - v.y)
504}
505
506fn scale(v: Point, k: f32) -> Point {
507 Point::new(v.x * k, v.y * k)
508}
509
510fn lerp(a: Point, b: Point, t: f32) -> Point {
511 Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
512}
513
514fn distance(a: Point, b: Point) -> f32 {
515 ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
516}
517
518fn dedup(contour: &[Point]) -> Vec<Point> {
519 let mut out: Vec<Point> = Vec::with_capacity(contour.len());
520 for &p in contour {
521 if out.last().is_none_or(|&last| distance(last, p) > 1e-5) {
522 out.push(p);
523 }
524 }
525 out
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 fn extents(strip: &[f32]) -> (f32, f32, f32, f32) {
533 let xs: Vec<f32> = strip.iter().step_by(2).copied().collect();
534 let ys: Vec<f32> = strip.iter().skip(1).step_by(2).copied().collect();
535 (
536 xs.iter().copied().fold(f32::MAX, f32::min),
537 ys.iter().copied().fold(f32::MAX, f32::min),
538 xs.iter().copied().fold(f32::MIN, f32::max),
539 ys.iter().copied().fold(f32::MIN, f32::max),
540 )
541 }
542
543 fn open(points: Vec<Point>) -> Vec<Contour> {
544 vec![Contour {
545 points,
546 closed: false,
547 has_segments: true,
548 }]
549 }
550
551 #[test]
556 fn a_move_only_contour_never_strokes_but_a_zero_length_segment_does() {
557 let at = Point::new(10.0, 10.0);
558 let move_only = vec![Contour {
559 points: vec![at],
560 closed: false,
561 has_segments: false,
562 }];
563 let zero_length = vec![Contour {
564 points: vec![at, at],
565 closed: false,
566 has_segments: true,
567 }];
568 let move_and_close = vec![Contour {
569 points: vec![at],
570 closed: true,
571 has_segments: true,
572 }];
573 for cap in [Cap::Butt, Cap::Round, Cap::Square] {
574 let stroke = Stroke {
575 cap,
576 ..Stroke::new(8.0)
577 };
578 assert!(
579 stroke_strip(&move_only, &stroke, 0.25).is_empty(),
580 "a bare move_to must paint nothing under {cap:?}"
581 );
582 }
583 for cap in [Cap::Round, Cap::Square] {
585 let stroke = Stroke {
586 cap,
587 ..Stroke::new(8.0)
588 };
589 assert_eq!(
590 stroke_strip(&move_and_close, &stroke, 0.25),
591 stroke_strip(&zero_length, &stroke, 0.25),
592 "move+close must stroke like an explicit zero-length segment ({cap:?})"
593 );
594 }
595
596 let butt = Stroke {
597 cap: Cap::Butt,
598 ..Stroke::new(8.0)
599 };
600 assert!(
601 stroke_strip(&zero_length, &butt, 0.25).is_empty(),
602 "a butt cap has no area to give a zero-length segment"
603 );
604 for cap in [Cap::Round, Cap::Square] {
605 let stroke = Stroke {
606 cap,
607 ..Stroke::new(8.0)
608 };
609 let strip = stroke_strip(&zero_length, &stroke, 0.25);
610 assert!(
611 !strip.is_empty(),
612 "{cap:?} must paint a zero-length segment"
613 );
614 let (x0, y0, x1, y1) = extents(&strip);
615 assert!(
616 (x0 - 6.0).abs() < 0.01
617 && (y0 - 6.0).abs() < 0.01
618 && (x1 - 14.0).abs() < 0.01
619 && (y1 - 14.0).abs() < 0.01,
620 "{cap:?} should span the full stroke width, got {:?}",
621 (x0, y0, x1, y1)
622 );
623 }
624 }
625
626 #[test]
629 fn the_flattener_records_whether_a_contour_ever_moved() {
630 use crate::PathBuilder;
631
632 let mut move_only = PathBuilder::new();
633 move_only.move_to((10.0, 10.0));
634 let flattened = move_only.build().flatten(0.25);
635 assert_eq!(flattened.len(), 1);
636 assert!(!flattened[0].has_segments);
637
638 let mut zero_length = PathBuilder::new();
639 zero_length.move_to((10.0, 10.0));
640 zero_length.line_to((10.0, 10.0));
641 let flattened = zero_length.build().flatten(0.25);
642 assert_eq!(flattened.len(), 1);
643 assert!(flattened[0].has_segments);
644
645 let mut move_and_close = PathBuilder::new();
650 move_and_close.move_to((10.0, 10.0));
651 move_and_close.close();
652 let flattened = move_and_close.build().flatten(0.25);
653 assert_eq!(flattened.len(), 1);
654 assert!(
655 flattened[0].has_segments,
656 "close draws; a bare move does not"
657 );
658
659 let mut mixed = PathBuilder::new();
662 mixed.move_to((0.0, 0.0));
663 mixed.line_to((10.0, 0.0));
664 mixed.move_to((50.0, 50.0));
665 let flattened = mixed.build().flatten(0.25);
666 assert_eq!(flattened.len(), 2);
667 assert!(flattened[0].has_segments);
668 assert!(!flattened[1].has_segments);
669 }
670
671 #[test]
677 fn a_zero_length_on_interval_puts_a_dot_at_the_path_start() {
678 let line = open(vec![Point::new(0.0, 50.0), Point::new(22.0, 50.0)]);
682 let dashes = dash_contours(
683 &line,
684 &Dash {
685 intervals: vec![0.0, 6.0],
686 phase: 0.0,
687 },
688 );
689 let positions: Vec<f32> = dashes.iter().map(|contour| contour.points[0].x).collect();
690 assert_eq!(positions, vec![0.0, 6.0, 12.0, 18.0]);
691 assert!(
692 dashes.iter().all(|contour| contour.has_segments),
693 "a zero-length on dash is real geometry and must keep its caps"
694 );
695 }
696
697 #[test]
703 fn the_endpoint_dot_follows_browsers_not_the_spec() {
704 let line = open(vec![Point::new(0.0, 50.0), Point::new(24.0, 50.0)]);
705 let dashes = dash_contours(
706 &line,
707 &Dash {
708 intervals: vec![0.0, 6.0],
709 phase: 0.0,
710 },
711 );
712 let positions: Vec<f32> = dashes.iter().map(|c| c.points[0].x).collect();
713 assert_eq!(
714 positions,
715 vec![0.0, 6.0, 12.0, 18.0],
716 "the dot at 24 is the spec's, not the browser's"
717 );
718 }
719
720 #[test]
724 fn an_ordinary_interval_boundary_gains_no_extra_dash() {
725 assert_eq!(interval_at(&[10.0, 6.0], 10.0), (1, 6.0));
726 assert_eq!(interval_at(&[10.0, 6.0], 0.0), (0, 10.0));
727 assert_eq!(interval_at(&[10.0, 6.0], 4.0), (0, 6.0));
728 assert_eq!(interval_at(&[0.0, 6.0], 0.0), (0, 0.0));
729 }
730
731 #[test]
732 fn stroke_contains_answers_inside_the_ink_and_nowhere_else() {
733 let line = open(vec![Point::new(10.0, 50.0), Point::new(90.0, 50.0)]);
734 let stroke = Stroke::new(10.0);
735 assert!(stroke_contains(
736 &line,
737 &stroke,
738 0.25,
739 Point::new(50.0, 50.0)
740 ));
741 assert!(stroke_contains(
742 &line,
743 &stroke,
744 0.25,
745 Point::new(50.0, 54.0)
746 ));
747 assert!(!stroke_contains(
750 &line,
751 &stroke,
752 0.25,
753 Point::new(50.0, 62.0)
754 ));
755 assert!(!stroke_contains(
757 &line,
758 &stroke,
759 0.25,
760 Point::new(95.0, 50.0)
761 ));
762 }
763
764 #[test]
765 fn a_wider_stroke_reaches_further() {
766 let line = open(vec![Point::new(10.0, 50.0), Point::new(90.0, 50.0)]);
767 let point = Point::new(50.0, 58.0);
768 assert!(!stroke_contains(&line, &Stroke::new(10.0), 0.25, point));
769 assert!(stroke_contains(&line, &Stroke::new(24.0), 0.25, point));
770 }
771
772 #[test]
776 fn a_second_contour_does_not_make_everything_hit() {
777 let two = vec![
778 Contour {
779 points: vec![Point::new(10.0, 20.0), Point::new(90.0, 20.0)],
780 closed: false,
781 has_segments: true,
782 },
783 Contour {
784 points: vec![Point::new(10.0, 80.0), Point::new(90.0, 80.0)],
785 closed: false,
786 has_segments: true,
787 },
788 ];
789 let stroke = Stroke::new(10.0);
790 assert!(stroke_contains(&two, &stroke, 0.25, Point::new(50.0, 20.0)));
791 assert!(stroke_contains(&two, &stroke, 0.25, Point::new(50.0, 80.0)));
792 assert!(!stroke_contains(
794 &two,
795 &stroke,
796 0.25,
797 Point::new(50.0, 50.0)
798 ));
799 assert!(!stroke_contains(
800 &two,
801 &stroke,
802 0.25,
803 Point::new(5000.0, 5000.0)
804 ));
805 }
806
807 #[test]
810 fn dash_gaps_are_not_part_of_the_stroke() {
811 let dashed = dash_contours(
812 &open(vec![Point::new(0.0, 50.0), Point::new(100.0, 50.0)]),
813 &Dash {
814 intervals: vec![10.0, 10.0],
815 phase: 0.0,
816 },
817 );
818 assert!(
819 dashed.len() > 2,
820 "the pattern has to produce several dashes"
821 );
822 let stroke = Stroke::new(10.0);
823 assert!(stroke_contains(
825 &dashed,
826 &stroke,
827 0.25,
828 Point::new(5.0, 50.0)
829 ));
830 assert!(!stroke_contains(
831 &dashed,
832 &stroke,
833 0.25,
834 Point::new(15.0, 50.0)
835 ));
836 assert!(stroke_contains(
837 &dashed,
838 &stroke,
839 0.25,
840 Point::new(25.0, 50.0)
841 ));
842 assert!(!stroke_contains(
843 &dashed,
844 &stroke,
845 0.25,
846 Point::new(50.0, 200.0)
847 ));
848 }
849
850 fn hline() -> Vec<Contour> {
851 open(vec![Point::new(10.0, 50.0), Point::new(110.0, 50.0)])
852 }
853
854 #[test]
855 fn butt_caps_stop_at_the_endpoints() {
856 let strip = stroke_strip(&hline(), &Stroke::new(10.0), 0.25);
857 let (x0, y0, x1, y1) = extents(&strip);
858 assert_eq!((x0, x1), (10.0, 110.0));
859 assert_eq!((y0, y1), (45.0, 55.0));
860 }
861
862 #[test]
863 fn square_and_round_caps_extend_half_width() {
864 for cap in [Cap::Square, Cap::Round] {
865 let stroke = Stroke {
866 cap,
867 ..Stroke::new(10.0)
868 };
869 let (x0, _, x1, _) = extents(&stroke_strip(&hline(), &stroke, 0.25));
870 assert!((x0 - 5.0).abs() < 0.3, "{cap:?} start: {x0}");
871 assert!((x1 - 115.0).abs() < 0.3, "{cap:?} end: {x1}");
872 }
873 }
874
875 #[test]
876 fn miter_spikes_until_the_limit_bevels() {
877 let angle = open(vec![
879 Point::new(0.0, 100.0),
880 Point::new(100.0, 100.0),
881 Point::new(100.0, 0.0),
882 ]);
883 let diagonal = |strip: &[f32]| {
884 strip
885 .chunks_exact(2)
886 .map(|v| v[0] + v[1])
887 .fold(f32::MIN, f32::max)
888 };
889 let strip = stroke_strip(&angle, &Stroke::new(20.0), 0.25);
890 assert!(
891 (diagonal(&strip) - 220.0).abs() < 0.1,
892 "miter tip reaches (110,110): {}",
893 diagonal(&strip)
894 );
895
896 let bevel = Stroke {
898 miter_limit: 1.0,
899 ..Stroke::new(20.0)
900 };
901 let strip = stroke_strip(&angle, &bevel, 0.25);
902 assert!(
903 diagonal(&strip) <= 210.0 + 0.1,
904 "beveled corner: {}",
905 diagonal(&strip)
906 );
907 }
908
909 #[test]
910 fn dash_splits_by_length() {
911 let dashed = dash_contours(
912 &hline(),
913 &Dash {
914 intervals: vec![30.0, 20.0],
915 phase: 0.0,
916 },
917 );
918 assert_eq!(dashed.len(), 2, "100px line, 30on/20off: {dashed:?}");
919 assert_eq!(dashed[0].points[0].x, 10.0);
920 assert!((dashed[0].points.last().unwrap().x - 40.0).abs() < 0.01);
921 assert!((dashed[1].points[0].x - 60.0).abs() < 0.01);
922 assert!((dashed[1].points.last().unwrap().x - 90.0).abs() < 0.01);
923 }
924
925 #[test]
926 fn odd_interval_dash_alternates_across_the_doubled_cycle() {
927 let dashed = dash_contours(
929 &hline(),
930 &Dash {
931 intervals: vec![30.0],
932 phase: 30.0,
933 },
934 );
935 assert_eq!(dashed.len(), 2, "{dashed:?}");
936 assert!((dashed[0].points[0].x - 40.0).abs() < 0.01, "{dashed:?}");
937 assert!((dashed[1].points[0].x - 100.0).abs() < 0.01, "{dashed:?}");
938 }
939
940 #[test]
941 fn invalid_dash_patterns_disable_dashing() {
942 for intervals in [vec![], vec![-5.0, 10.0], vec![0.0, 0.0]] {
943 let dashed = dash_contours(
944 &hline(),
945 &Dash {
946 intervals,
947 phase: 0.0,
948 },
949 );
950 assert_eq!(dashed.len(), 1, "pattern passes through as solid");
951 assert_eq!(dashed[0].points.len(), 2);
952 }
953 }
954
955 #[test]
956 fn closed_contour_has_no_caps_and_wraps_joins() {
957 let square = vec![Contour {
958 points: vec![
959 Point::new(0.0, 0.0),
960 Point::new(100.0, 0.0),
961 Point::new(100.0, 100.0),
962 Point::new(0.0, 100.0),
963 Point::new(0.0, 0.0),
964 ],
965 closed: true,
966 has_segments: true,
967 }];
968 let strip = stroke_strip(&square, &Stroke::new(10.0), 0.25);
969 let (x0, y0, x1, y1) = extents(&strip);
970 assert_eq!((x0, y0, x1, y1), (-5.0, -5.0, 105.0, 105.0));
972 }
973
974 #[test]
975 fn closure_is_metadata_not_point_coincidence() {
976 let points = vec![
979 Point::new(0.0, 0.0),
980 Point::new(100.0, 0.0),
981 Point::new(100.0, 100.0),
982 Point::new(0.0, 100.0),
983 Point::new(0.0, 0.0),
984 ];
985 let by_flag = |closed: bool| {
986 stroke_strip(
987 &[Contour {
988 points: points.clone(),
989 closed,
990 has_segments: true,
991 }],
992 &Stroke::new(10.0),
993 0.25,
994 )
995 };
996 assert_ne!(
997 by_flag(true).len(),
998 by_flag(false).len(),
999 "seam treatment must come from the flag"
1000 );
1001 }
1002
1003 #[test]
1008 fn a_zero_length_subpath_paints_only_for_extending_caps() {
1009 let dot = |cap| {
1010 let contour = Contour {
1011 points: vec![Point::new(8.0, 8.0), Point::new(8.0, 8.0)],
1012 closed: false,
1013 has_segments: true,
1014 };
1015 let mut stroke = Stroke::new(4.0);
1016 stroke.cap = cap;
1017 stroke_strip(&[contour], &stroke, 0.25)
1018 };
1019 assert!(dot(Cap::Butt).is_empty(), "butt caps enclose no area");
1020 assert!(
1021 !dot(Cap::Square).is_empty(),
1022 "square extends past the point"
1023 );
1024 assert!(!dot(Cap::Round).is_empty(), "round extends past the point");
1025 }
1026
1027 #[test]
1030 fn butt_capped_ink_is_continuous_as_a_segment_vanishes() {
1031 let area_at = |length: f32| {
1032 let contour = Contour {
1033 points: vec![Point::new(8.0, 8.0), Point::new(8.0 + length, 8.0)],
1034 closed: false,
1035 has_segments: true,
1036 };
1037 let mut stroke = Stroke::new(4.0);
1038 stroke.cap = Cap::Butt;
1039 let strip = stroke_strip(&[contour], &stroke, 0.25);
1040 let (x0, y0, x1, y1) = extents(&strip);
1041 if strip.is_empty() {
1042 0.0
1043 } else {
1044 (x1 - x0) * (y1 - y0)
1045 }
1046 };
1047 assert!(
1048 area_at(0.001) < 0.05,
1049 "a hair-thin segment paints hardly anything"
1050 );
1051 assert_eq!(area_at(0.0), 0.0, "and zero paints nothing at all");
1052 }
1053}