1use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathSeg, QuadBez};
2
3const PATH_TRIM_ACCURACY: f64 = 0.1;
4
5#[derive(Clone, Copy)]
6struct TrimSegment {
7 segment: PathSeg,
8 contour_index: usize,
9 closes_contour: bool,
10}
11
12#[derive(Clone, Copy)]
13struct TrimContour {
14 first_segment_index: Option<usize>,
15 last_segment_index: Option<usize>,
16 is_closed: bool,
17}
18
19pub fn trim_bez_path(path: &BezPath, draw_start: f64, draw_end: f64) -> BezPath {
21 let start = draw_start.clamp(0.0, 1.0);
22 let end = draw_end.clamp(0.0, 1.0);
23 if start <= f64::EPSILON && end >= 1.0 - f64::EPSILON {
24 return path.clone();
25 }
26 if start >= end {
27 return BezPath::new();
28 }
29
30 let (segments, contours) = collect_trim_segments(path);
31 let lengths = segments
32 .iter()
33 .map(|record| record.segment.arclen(PATH_TRIM_ACCURACY).max(0.0))
34 .collect::<Vec<_>>();
35 let total_length = lengths.iter().sum::<f64>();
36 if total_length <= f64::EPSILON {
37 return BezPath::new();
38 }
39
40 let trim_start = start * total_length;
41 let trim_end = end * total_length;
42 let mut trimmed = BezPath::new();
43 let mut current_point = None;
44 let mut cursor = 0.0;
45 let mut segment_starts = Vec::with_capacity(lengths.len());
46 let mut segment_ends = Vec::with_capacity(lengths.len());
47
48 for segment_length in &lengths {
49 segment_starts.push(cursor);
50 cursor += segment_length;
51 segment_ends.push(cursor);
52 }
53
54 let mut fully_visible_closed_contours = vec![false; contours.len()];
55 for (contour_index, contour) in contours.iter().enumerate() {
56 if !contour.is_closed {
57 continue;
58 }
59 let (Some(first_segment_index), Some(last_segment_index)) =
60 (contour.first_segment_index, contour.last_segment_index)
61 else {
62 continue;
63 };
64 let contour_start = segment_starts[first_segment_index];
65 let contour_end = segment_ends[last_segment_index];
66 fully_visible_closed_contours[contour_index] =
67 trim_start <= contour_start + f64::EPSILON && trim_end >= contour_end - f64::EPSILON;
68 }
69
70 for (index, (record, segment_length)) in segments.into_iter().zip(lengths).enumerate() {
71 let segment_start = segment_starts[index];
72 let segment_end = segment_ends[index];
73 if segment_length <= f64::EPSILON || segment_end <= trim_start + f64::EPSILON {
74 continue;
75 }
76 if segment_start >= trim_end - f64::EPSILON {
77 break;
78 }
79
80 let local_start = (trim_start - segment_start).clamp(0.0, segment_length);
81 let local_end = (trim_end - segment_start).clamp(0.0, segment_length);
82 if local_start >= local_end {
83 continue;
84 }
85
86 let whole_segment_visible =
87 local_start <= f64::EPSILON && local_end >= segment_length - f64::EPSILON;
88 let visible_segment = if whole_segment_visible {
89 record.segment
90 } else {
91 let t0 = if local_start <= f64::EPSILON {
92 0.0
93 } else {
94 record.segment.inv_arclen(local_start, PATH_TRIM_ACCURACY)
95 };
96 let t1 = if local_end >= segment_length - f64::EPSILON {
97 1.0
98 } else {
99 record.segment.inv_arclen(local_end, PATH_TRIM_ACCURACY)
100 };
101 if t0 >= t1 {
102 continue;
103 }
104 record.segment.subsegment(t0..t1)
105 };
106
107 append_segment(&mut trimmed, &mut current_point, visible_segment);
108 if whole_segment_visible
109 && record.closes_contour
110 && fully_visible_closed_contours[record.contour_index]
111 {
112 trimmed.close_path();
113 current_point = None;
114 }
115 }
116
117 trimmed
118}
119
120fn collect_trim_segments(path: &BezPath) -> (Vec<TrimSegment>, Vec<TrimContour>) {
121 let mut segments = Vec::new();
122 let mut contours = Vec::new();
123 let mut current_contour = None;
124 let mut contour_start = None;
125 let mut current_point = None;
126
127 for element in path.iter() {
128 match element {
129 kurbo::PathEl::MoveTo(point) => {
130 current_contour = Some(contours.len());
131 contours.push(TrimContour {
132 first_segment_index: None,
133 last_segment_index: None,
134 is_closed: false,
135 });
136 contour_start = Some(point);
137 current_point = Some(point);
138 }
139 kurbo::PathEl::LineTo(point) => {
140 let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
141 continue;
142 };
143 push_trim_segment(
144 &mut segments,
145 &mut contours,
146 contour_index,
147 PathSeg::Line(Line::new(from, point)),
148 false,
149 );
150 current_point = Some(point);
151 }
152 kurbo::PathEl::QuadTo(control, point) => {
153 let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
154 continue;
155 };
156 push_trim_segment(
157 &mut segments,
158 &mut contours,
159 contour_index,
160 PathSeg::Quad(QuadBez::new(from, control, point)),
161 false,
162 );
163 current_point = Some(point);
164 }
165 kurbo::PathEl::CurveTo(control_1, control_2, point) => {
166 let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
167 continue;
168 };
169 push_trim_segment(
170 &mut segments,
171 &mut contours,
172 contour_index,
173 PathSeg::Cubic(CubicBez::new(from, control_1, control_2, point)),
174 false,
175 );
176 current_point = Some(point);
177 }
178 kurbo::PathEl::ClosePath => {
179 let (Some(from), Some(start), Some(contour_index)) =
180 (current_point, contour_start, current_contour)
181 else {
182 continue;
183 };
184 contours[contour_index].is_closed = true;
185 if same_kurbo_point(from, start) {
186 if let Some(last_segment_index) = contours[contour_index].last_segment_index {
187 segments[last_segment_index].closes_contour = true;
188 }
189 } else {
190 push_trim_segment(
191 &mut segments,
192 &mut contours,
193 contour_index,
194 PathSeg::Line(Line::new(from, start)),
195 true,
196 );
197 current_point = Some(start);
198 }
199 }
200 }
201 }
202
203 (segments, contours)
204}
205
206fn push_trim_segment(
207 segments: &mut Vec<TrimSegment>,
208 contours: &mut [TrimContour],
209 contour_index: usize,
210 segment: PathSeg,
211 closes_contour: bool,
212) {
213 let segment_index = segments.len();
214 let contour = &mut contours[contour_index];
215 if contour.first_segment_index.is_none() {
216 contour.first_segment_index = Some(segment_index);
217 }
218 contour.last_segment_index = Some(segment_index);
219 segments.push(TrimSegment {
220 segment,
221 contour_index,
222 closes_contour,
223 });
224}
225
226fn append_segment(path: &mut BezPath, current_point: &mut Option<kurbo::Point>, segment: PathSeg) {
227 let start = segment.start();
228 if current_point
229 .map(|point| !same_kurbo_point(point, start))
230 .unwrap_or(true)
231 {
232 path.move_to(start);
233 }
234 path.push(segment.as_path_el());
235 *current_point = Some(segment.end());
236}
237
238fn same_kurbo_point(a: kurbo::Point, b: kurbo::Point) -> bool {
239 (a.x - b.x).abs() <= f64::EPSILON && (a.y - b.y).abs() <= f64::EPSILON
240}