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