pax_runtime_api/drawing/
path_smoothing.rs1use kurbo::{BezPath, PathEl, Point};
2
3use super::PathSmoothing;
4
5const EPSILON: f64 = 1e-6;
6
7#[derive(Clone, Copy)]
8struct SmoothingParams {
9 angle_cut_degrees: f64,
10 passes: usize,
11}
12
13pub fn smooth_bez_path(path: &BezPath, smoothing: PathSmoothing) -> BezPath {
18 let Some(params) = smoothing_params(smoothing) else {
19 return path.clone();
20 };
21
22 let mut output = BezPath::new();
23 let mut run = Vec::new();
24 let mut contour_start = None;
25
26 for element in path.elements() {
27 match *element {
28 PathEl::MoveTo(point) => {
29 flush_run(&mut output, &mut run, params);
30 output.move_to(point);
31 run.clear();
32 run.push(point);
33 contour_start = Some(point);
34 }
35 PathEl::LineTo(point) => {
36 if run.is_empty() {
37 output.line_to(point);
38 } else {
39 push_distinct(&mut run, point);
40 }
41 }
42 PathEl::QuadTo(control, point) => {
43 flush_run(&mut output, &mut run, params);
44 output.quad_to(control, point);
45 run.clear();
46 run.push(point);
47 }
48 PathEl::CurveTo(control_1, control_2, point) => {
49 flush_run(&mut output, &mut run, params);
50 output.curve_to(control_1, control_2, point);
51 run.clear();
52 run.push(point);
53 }
54 PathEl::ClosePath => {
55 if let (Some(start), Some(last)) = (contour_start, run.last().copied()) {
56 if distance(last, start) > EPSILON {
57 run.push(start);
58 }
59 }
60 flush_run(&mut output, &mut run, params);
61 output.close_path();
62 run.clear();
63 contour_start = None;
64 }
65 }
66 }
67
68 flush_run(&mut output, &mut run, params);
69 output
70}
71
72fn smoothing_params(smoothing: PathSmoothing) -> Option<SmoothingParams> {
73 match smoothing {
74 PathSmoothing::None => None,
75 PathSmoothing::Light => Some(SmoothingParams {
76 angle_cut_degrees: 130.0,
77 passes: 1,
78 }),
79 PathSmoothing::Strong => Some(SmoothingParams {
80 angle_cut_degrees: 95.0,
81 passes: 2,
82 }),
83 }
84}
85
86fn flush_run(output: &mut BezPath, run: &mut Vec<Point>, params: SmoothingParams) {
87 let clean = clean_points(run);
88 if clean.len() < 2 {
89 run.clear();
90 return;
91 }
92
93 let mut cuts = vec![0usize];
94 for index in 1..clean.len() - 1 {
95 if angle_degrees(clean[index - 1], clean[index], clean[index + 1])
96 <= params.angle_cut_degrees
97 {
98 cuts.push(index);
99 }
100 }
101 cuts.push(clean.len() - 1);
102
103 for pair in cuts.windows(2) {
104 let start = pair[0];
105 let end = pair[1];
106 if end <= start {
107 continue;
108 }
109 let subrun = smooth_points(&clean[start..=end], params.passes);
110 emit_subrun(output, &subrun);
111 }
112
113 run.clear();
114}
115
116fn emit_subrun(output: &mut BezPath, points: &[Point]) {
117 if points.len() < 2 {
118 return;
119 }
120 if points.len() == 2 {
121 output.line_to(points[1]);
122 return;
123 }
124
125 for index in 0..points.len() - 1 {
126 let p0 = if index == 0 {
127 points[index]
128 } else {
129 points[index - 1]
130 };
131 let p1 = points[index];
132 let p2 = points[index + 1];
133 let p3 = if index + 2 < points.len() {
134 points[index + 2]
135 } else {
136 points[index + 1]
137 };
138
139 let control_1 = p1 + (p2 - p0) * (1.0 / 6.0);
140 let control_2 = p2 - (p3 - p1) * (1.0 / 6.0);
141 output.curve_to(control_1, control_2, p2);
142 }
143}
144
145fn smooth_points(points: &[Point], passes: usize) -> Vec<Point> {
146 if points.len() < 4 || passes == 0 {
147 return points.to_vec();
148 }
149
150 let mut output = points.to_vec();
151 for _ in 0..passes {
152 let mut next = Vec::with_capacity(output.len());
153 next.push(output[0]);
154 for index in 1..output.len() - 1 {
155 let point = Point::new(
156 output[index - 1].x * 0.25 + output[index].x * 0.5 + output[index + 1].x * 0.25,
157 output[index - 1].y * 0.25 + output[index].y * 0.5 + output[index + 1].y * 0.25,
158 );
159 next.push(point);
160 }
161 next.push(output[output.len() - 1]);
162 output = next;
163 }
164 output
165}
166
167fn clean_points(points: &[Point]) -> Vec<Point> {
168 let mut clean = Vec::with_capacity(points.len());
169 for point in points {
170 push_distinct(&mut clean, *point);
171 }
172 clean
173}
174
175fn push_distinct(points: &mut Vec<Point>, point: Point) {
176 if points
177 .last()
178 .map(|last| distance(*last, point) > EPSILON)
179 .unwrap_or(true)
180 {
181 points.push(point);
182 }
183}
184
185fn angle_degrees(previous: Point, current: Point, next: Point) -> f64 {
186 let vector_1 = previous - current;
187 let vector_2 = next - current;
188 let length_1 = vector_1.hypot();
189 let length_2 = vector_2.hypot();
190 if length_1 < EPSILON || length_2 < EPSILON {
191 return 180.0;
192 }
193 let dot = ((vector_1.x * vector_2.x) + (vector_1.y * vector_2.y)) / (length_1 * length_2);
194 dot.clamp(-1.0, 1.0).acos().to_degrees()
195}
196
197fn distance(a: Point, b: Point) -> f64 {
198 (a - b).hypot()
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn none_preserves_path() {
207 let mut path = BezPath::new();
208 path.move_to((0.0, 0.0));
209 path.line_to((50.0, 20.0));
210 path.line_to((100.0, 0.0));
211
212 assert_eq!(smooth_bez_path(&path, PathSmoothing::None), path);
213 }
214
215 #[test]
216 fn smoothing_converts_gentle_line_run_to_curves() {
217 let mut path = BezPath::new();
218 path.move_to((0.0, 0.0));
219 path.line_to((25.0, 10.0));
220 path.line_to((50.0, 12.0));
221 path.line_to((75.0, 10.0));
222 path.line_to((100.0, 0.0));
223
224 let smoothed = smooth_bez_path(&path, PathSmoothing::Light);
225
226 assert!(smoothed
227 .elements()
228 .iter()
229 .any(|element| matches!(element, PathEl::CurveTo(..))));
230 }
231
232 #[test]
233 fn smoothing_preserves_sharp_corners_as_lines() {
234 let mut path = BezPath::new();
235 path.move_to((0.0, 0.0));
236 path.line_to((50.0, 0.0));
237 path.line_to((50.0, 50.0));
238
239 let smoothed = smooth_bez_path(&path, PathSmoothing::Light);
240
241 assert!(smoothed
242 .elements()
243 .iter()
244 .any(|element| matches!(element, PathEl::LineTo(..))));
245 }
246}