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