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::{Stroke, StrokeCap, StrokeJoin};
7
8pub fn stroke_width_pixels(stroke: &Stroke) -> f64 {
10 stroke.width.get().expect_pixels().to_float()
11}
12
13pub fn stroked_outline_path(centerline: &BezPath, stroke: &Stroke) -> 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.get()))
27 .with_miter_limit(4.0)
28 .with_caps(to_kurbo_cap(stroke.cap.get())),
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(centerline: &BezPath, stroke: &Stroke, width: f64) -> Option<BezPath> {
60 let center = collapsed_path_point(centerline)?;
61 let half_width = width / 2.0;
62
63 match stroke.cap.get() {
64 StrokeCap::Butt => None,
65 StrokeCap::Round => Some(Circle::new(center, half_width).to_path(0.1)),
66 StrokeCap::Square => Some(
67 Rect::new(
68 center.x - half_width,
69 center.y - half_width,
70 center.x + half_width,
71 center.y + half_width,
72 )
73 .to_path(0.1),
74 ),
75 }
76}
77
78fn collapsed_path_point(centerline: &BezPath) -> Option<Point> {
79 let mut anchor = None;
80
81 for element in centerline.elements() {
84 let points = match *element {
85 PathEl::MoveTo(point) | PathEl::LineTo(point) => [Some(point), None, None],
86 PathEl::QuadTo(point1, point2) => [Some(point1), Some(point2), None],
87 PathEl::CurveTo(point1, point2, point3) => [Some(point1), Some(point2), Some(point3)],
88 PathEl::ClosePath => [None, None, None],
89 };
90
91 for point in points.into_iter().flatten() {
92 match anchor {
93 Some(existing) if !same_point(existing, point) => return None,
94 Some(_) => {}
95 None => anchor = Some(point),
96 }
97 }
98 }
99
100 anchor
101}
102
103fn same_point(a: Point, b: Point) -> bool {
104 (a.x - b.x).abs() <= f64::EPSILON && (a.y - b.y).abs() <= f64::EPSILON
105}