alpha2_geometry/
stroke.rs1use alpha2_model::{LineCap, LineJoin};
5use kurbo::{Affine, BezPath, Cap, Join, Stroke, StrokeOpts};
6
7const STROKE_TOLERANCE: f64 = 0.05;
10
11const MAX_DASH_REPEATS: f64 = 100_000.0;
14
15fn control_length(path: &BezPath) -> f64 {
17 let mut total = 0.0;
18 let mut last = kurbo::Point::ZERO;
19 let mut start = kurbo::Point::ZERO;
20 for element in path.elements() {
21 let points: &[kurbo::Point] = match element {
22 kurbo::PathEl::MoveTo(p) => {
23 last = *p;
24 start = *p;
25 continue;
26 }
27 kurbo::PathEl::LineTo(p) => std::slice::from_ref(p),
28 kurbo::PathEl::QuadTo(a, b) => &[*a, *b],
29 kurbo::PathEl::CurveTo(a, b, c) => &[*a, *b, *c],
30 kurbo::PathEl::ClosePath => std::slice::from_ref(&start),
31 };
32 for point in points {
33 total += crate::affine::distance(last, *point);
34 last = *point;
35 }
36 }
37 total
38}
39
40#[derive(Clone, Debug, PartialEq)]
42pub struct StrokeStyle {
43 pub width: f64,
45 pub join: LineJoin,
47 pub miter_limit: f64,
49 pub cap: LineCap,
51 pub dash: Vec<f64>,
53 pub dash_offset: f64,
55}
56
57#[must_use]
62pub fn stroke_outline(path: &BezPath, style: &StrokeStyle, to_device: Affine) -> BezPath {
63 if style.width.is_nan() || style.width <= 0.0 || path.elements().is_empty() {
64 return BezPath::new();
65 }
66 let join = match style.join {
67 LineJoin::Miter => Join::Miter,
68 LineJoin::Round => Join::Round,
69 LineJoin::Bevel => Join::Bevel,
70 };
71 let cap = match style.cap {
72 LineCap::Butt => Cap::Butt,
73 LineCap::Round => Cap::Round,
74 LineCap::Square => Cap::Square,
75 };
76 let mut stroke = Stroke::new(style.width)
77 .with_join(join)
78 .with_miter_limit(style.miter_limit.max(1.0))
79 .with_caps(cap);
80 let pattern_length: f64 = style.dash.iter().sum();
81 let repeats = control_length(path) / pattern_length;
84 if pattern_length > 0.0 && repeats <= MAX_DASH_REPEATS {
85 let mut pattern = style.dash.clone();
87 if pattern.len() % 2 == 1 {
88 pattern.extend_from_slice(&style.dash);
89 }
90 stroke = stroke.with_dashes(style.dash_offset, pattern);
91 }
92 let scale = crate::affine::max_scale(to_device).max(1e-9);
94 let outline = kurbo::stroke(
95 path.iter(),
96 &stroke,
97 &StrokeOpts::default(),
98 STROKE_TOLERANCE / scale,
99 );
100 to_device * outline
101}