pax_runtime_api/drawing/
stroke_utils.rs1use kurbo::{
2 stroke as kurbo_stroke, BezPath, Cap as KurboCap, Circle, Join as KurboJoin, PathEl, Point,
3 Rect, Shape, Stroke as KurboStroke, StrokeOpts as KurboStrokeOpts,
4};
5
6use super::{ResolvedStroke, StrokeCap, StrokeJoin};
7
8pub fn stroke_width_pixels(stroke: &ResolvedStroke) -> f64 {
10 stroke.width_pixels()
11}
12
13pub fn stroked_outline_path(centerline: &BezPath, stroke: &ResolvedStroke) -> Option<BezPath> {
18 let width = stroke_width_pixels(stroke);
19 if width <= f64::EPSILON {
20 return None;
21 }
22
23 let outline = kurbo_stroke(
24 centerline.elements().iter().copied(),
25 &KurboStroke::new(width)
26 .with_join(to_kurbo_join(stroke.join.clone()))
27 .with_miter_limit(4.0)
28 .with_caps(to_kurbo_cap(stroke.cap.clone())),
29 &KurboStrokeOpts::default(),
30 0.1,
31 );
32
33 if !outline.elements().is_empty() {
34 Some(outline)
35 } else {
36 degenerate_outline_path(centerline, stroke, width)
40 }
41}
42
43fn to_kurbo_cap(cap: StrokeCap) -> KurboCap {
44 match cap {
45 StrokeCap::Butt => KurboCap::Butt,
46 StrokeCap::Round => KurboCap::Round,
47 StrokeCap::Square => KurboCap::Square,
48 }
49}
50
51fn to_kurbo_join(join: StrokeJoin) -> KurboJoin {
52 match join {
53 StrokeJoin::Miter => KurboJoin::Miter,
54 StrokeJoin::Round => KurboJoin::Round,
55 StrokeJoin::Bevel => KurboJoin::Bevel,
56 }
57}
58
59fn degenerate_outline_path(
60 centerline: &BezPath,
61 stroke: &ResolvedStroke,
62 width: f64,
63) -> Option<BezPath> {
64 let center = collapsed_path_point(centerline)?;
65 let half_width = width / 2.0;
66
67 match stroke.cap {
68 StrokeCap::Butt => None,
69 StrokeCap::Round => Some(Circle::new(center, half_width).to_path(0.1)),
70 StrokeCap::Square => Some(
71 Rect::new(
72 center.x - half_width,
73 center.y - half_width,
74 center.x + half_width,
75 center.y + half_width,
76 )
77 .to_path(0.1),
78 ),
79 }
80}
81
82fn collapsed_path_point(centerline: &BezPath) -> Option<Point> {
83 let mut anchor = None;
84
85 for element in centerline.elements() {
88 let points = match *element {
89 PathEl::MoveTo(point) | PathEl::LineTo(point) => [Some(point), None, None],
90 PathEl::QuadTo(point1, point2) => [Some(point1), Some(point2), None],
91 PathEl::CurveTo(point1, point2, point3) => [Some(point1), Some(point2), Some(point3)],
92 PathEl::ClosePath => [None, None, None],
93 };
94
95 for point in points.into_iter().flatten() {
96 match anchor {
97 Some(existing) if !same_point(existing, point) => return None,
98 Some(_) => {}
99 None => anchor = Some(point),
100 }
101 }
102 }
103
104 anchor
105}
106
107fn same_point(a: Point, b: Point) -> bool {
108 (a.x - b.x).abs() <= f64::EPSILON && (a.y - b.y).abs() <= f64::EPSILON
109}