1use std::num::NonZeroUsize;
6
7use crate::math;
8use crate::shapes::command_path::{Command, CommandPath, CommandPathBuilder};
9
10#[derive(Clone, Copy, Debug)]
11struct CubicSegment {
12 t: f32,
13 len: f32,
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17enum PathPartType {
18 Line,
19 Cubic(NonZeroUsize),
20}
21
22#[derive(Clone, Copy, Debug)]
23struct PathPart {
24 r#type: PathPartType,
25 offset: usize,
26 num_segments: usize,
27}
28
29impl PathPart {
30 pub fn line(offset: usize) -> Self {
31 Self { r#type: PathPartType::Line, offset, num_segments: 0 }
32 }
33
34 pub fn cubic(offset: usize) -> Self {
35 Self { r#type: PathPartType::Cubic(NonZeroUsize::new(1).unwrap()), offset, num_segments: 0 }
36 }
37}
38
39fn compute_hull(
40 from: math::Vec,
41 from_out: math::Vec,
42 to_in: math::Vec,
43 to: math::Vec,
44 t: f32,
45 hull: &mut [math::Vec; 6],
46) {
47 hull[0] = from.lerp(from_out, t);
48 hull[1] = from_out.lerp(to_in, t);
49 hull[2] = to_in.lerp(to, t);
50
51 hull[3] = hull[0].lerp(hull[1], t);
52 hull[4] = hull[1].lerp(hull[2], t);
53
54 hull[5] = hull[3].lerp(hull[4], t);
55}
56
57fn too_far(a: math::Vec, b: math::Vec) -> bool {
58 const TOO_FAR: f32 = 1.0;
59 (a.x - b.x).abs().max((a.y - b.y).abs()) > TOO_FAR
60}
61
62fn should_split_cubic(
63 from: math::Vec,
64 from_out: math::Vec,
65 to_in: math::Vec,
66 to: math::Vec,
67) -> bool {
68 let one_third = from.lerp(to, 1.0 / 3.0);
69 let two_thirds = from.lerp(to, 2.0 / 3.0);
70
71 too_far(from_out, one_third) || too_far(to_in, two_thirds)
72}
73
74#[allow(clippy::too_many_arguments)]
75fn segment_cubic(
76 from: math::Vec,
77 from_out: math::Vec,
78 to_in: math::Vec,
79 to: math::Vec,
80 mut running_length: f32,
81 t0: f32,
82 t1: f32,
83 segments: &mut Vec<CubicSegment>,
84) -> f32 {
85 const MIN_SEGMENT_LENGTH: f32 = 0.05;
86
87 if should_split_cubic(from, from_out, to_in, to) {
88 let half_t = (t0 + t1) / 2.0;
89
90 let mut hull = [math::Vec::default(); 6];
91 compute_hull(from, from_out, to_in, to, 0.5, &mut hull);
92
93 running_length =
94 segment_cubic(from, hull[0], hull[3], hull[5], running_length, t0, half_t, segments);
95 running_length =
96 segment_cubic(hull[5], hull[4], hull[2], to, running_length, half_t, t1, segments);
97 } else {
98 let length = from.distance(to);
99 running_length += length;
100
101 if length > MIN_SEGMENT_LENGTH {
102 segments.push(CubicSegment { t: t1, len: running_length });
103 }
104 }
105
106 running_length
107}
108
109#[derive(Debug)]
110pub struct MetricsPath {
111 points: Vec<math::Vec>,
112 cubic_segments: Vec<CubicSegment>,
113 parts: Vec<PathPart>,
114 lengths: Vec<f32>,
115}
116
117impl MetricsPath {
118 pub fn new(command_path: &CommandPath) -> Self {
119 let mut points = Vec::new();
120 let mut parts = Vec::new();
121
122 for command in &command_path.commands {
123 match *command {
124 Command::MoveTo(p) => points.push(p),
125 Command::LineTo(p) => {
126 parts.push(PathPart::line(points.len()));
127 points.push(p);
128 }
129 Command::CubicTo(c0, c1, p) => {
130 parts.push(PathPart::cubic(points.len()));
131 points.push(c0);
132 points.push(c1);
133 points.push(p);
134 }
135 Command::Close => {
136 if parts.last().map(|part| part.r#type) == Some(PathPartType::Line) {
137 parts.push(PathPart::line(points.len()));
140 points.push(points[0]);
141 }
142 }
143 }
144 }
145
146 Self { points, cubic_segments: Vec::new(), parts, lengths: Vec::new() }
147 }
148
149 pub fn compute_length(&mut self) -> f32 {
150 self.cubic_segments.clear();
151 self.lengths.clear();
152
153 let mut i = 0;
154 let mut length = 0.0;
155
156 for part in &mut self.parts {
157 match part.r#type {
158 PathPartType::Line => {
159 let from = self.points[i];
160 let to = self.points[i + 1];
161
162 i += 1;
163
164 let part_length = from.distance(to);
165 self.lengths.push(part_length);
166 length += part_length;
167 }
168 PathPartType::Cubic(ref mut ci) => {
169 let from = self.points[i];
170 let from_out = self.points[i + 1];
171 let to_in = self.points[i + 2];
172 let to = self.points[i + 3];
173
174 i += 3;
175
176 let index = self.cubic_segments.len();
177 *ci = NonZeroUsize::new(index + 1).unwrap();
178
179 let part_length = segment_cubic(
180 from,
181 from_out,
182 to_in,
183 to,
184 0.0,
185 0.0,
186 1.0,
187 &mut self.cubic_segments,
188 );
189 self.lengths.push(part_length);
190 length += part_length;
191 part.num_segments = self.cubic_segments.len() - index;
192 }
193 }
194 }
195
196 length
197 }
198
199 pub fn trimmed(
200 &self,
201 builder: &mut CommandPathBuilder,
202 start_len: f32,
203 end_len: f32,
204 move_to: bool,
205 ) {
206 assert!(end_len >= start_len);
207
208 if start_len == end_len || self.parts.is_empty() {
209 return;
210 }
211
212 let parts_and_lengths = self.lengths.iter().scan(0.0, |len, &part_len| {
213 let old_len = *len;
214 *len += part_len;
215
216 Some((old_len, part_len))
217 });
218
219 let first_part = parts_and_lengths
220 .clone()
221 .enumerate()
222 .find(|(_, (len, part_len))| len + part_len > start_len)
223 .map(|(i, (len, part_len))| (i, (start_len - len) / part_len));
224
225 if let Some((first_part_index, start_t)) = first_part {
226 let (last_part_index, end_t) = parts_and_lengths
227 .enumerate()
228 .skip(first_part_index)
229 .find(|(_, (len, part_len))| len + part_len >= end_len)
230 .map(|(i, (len, part_len))| (i, (end_len - len) / part_len))
231 .unwrap_or_else(|| (self.parts.len() - 1, 1.0));
232
233 let start_t = start_t.clamp(0.0, 1.0);
234 let end_t = end_t.clamp(0.0, 1.0);
235
236 if first_part_index == last_part_index {
237 self.extract_sub_part(first_part_index, start_t, end_t, move_to, builder);
238 } else {
239 self.extract_sub_part(first_part_index, start_t, 1.0, move_to, builder);
240
241 for part in &self.parts[first_part_index + 1..last_part_index] {
242 match part.r#type {
243 PathPartType::Line => {
244 builder.line_to(self.points[part.offset]);
245 }
246 PathPartType::Cubic(_) => {
247 builder.cubic_to(
248 self.points[part.offset],
249 self.points[part.offset + 1],
250 self.points[part.offset + 2],
251 );
252 }
253 }
254 }
255
256 self.extract_sub_part(last_part_index, 0.0, end_t, false, builder);
257 }
258 }
259 }
260
261 fn extract_sub_part(
262 &self,
263 i: usize,
264 mut start_t: f32,
265 mut end_t: f32,
266 move_to: bool,
267 builder: &mut CommandPathBuilder,
268 ) {
269 let part = self.parts[i];
270 match part.r#type {
271 PathPartType::Line => {
272 let from = self.points[part.offset - 1];
273 let to = self.points[part.offset];
274
275 let dir = to - from;
276
277 if move_to {
278 builder.move_to(from + dir * start_t);
279 }
280 builder.line_to(from + dir * end_t);
281 }
282 PathPartType::Cubic(ci) => {
283 let starting_segment_index = ci.get() - 1;
284 let mut start_end_segment_index = starting_segment_index;
285 let ending_segment_index = starting_segment_index + part.num_segments;
286
287 let len = self.lengths[i];
288 if start_t != 0.0 {
289 let start_len = start_t * len;
290 for si in starting_segment_index..ending_segment_index {
291 let segment = self.cubic_segments[si];
292 if segment.len >= start_len {
293 if si == starting_segment_index {
294 start_t = segment.t * (start_len / segment.len);
295 } else {
296 let previous_len = self.cubic_segments[si - 1].len;
297
298 let t = (start_len - previous_len) / (segment.len - previous_len);
299 start_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
300 }
301
302 start_end_segment_index = si;
306 break;
307 }
308 }
309 }
310
311 if end_t != 1.0 {
312 let end_len = end_t * len;
313 for si in start_end_segment_index..ending_segment_index {
314 let segment = self.cubic_segments[si];
315 if segment.len >= end_len {
316 if si == starting_segment_index {
317 end_t = segment.t * (end_len / segment.len);
318 } else {
319 let previous_len = self.cubic_segments[si - 1].len;
320
321 let t = (end_len - previous_len) / (segment.len - previous_len);
322 end_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
323 }
324
325 break;
326 }
327 }
328 }
329
330 let mut hull = [math::Vec::default(); 6];
331
332 let from = self.points[part.offset - 1];
333 let from_out = self.points[part.offset];
334 let to_in = self.points[part.offset + 1];
335 let to = self.points[part.offset + 2];
336
337 if start_t == 0.0 {
338 compute_hull(from, from_out, to_in, to, end_t, &mut hull);
339
340 if move_to {
341 builder.move_to(from);
342 }
343 builder.cubic_to(hull[0], hull[3], hull[5]);
344 } else {
345 compute_hull(from, from_out, to_in, to, start_t, &mut hull);
347
348 if move_to {
349 builder.move_to(hull[5]);
351 }
352 if end_t == 1.0 {
353 builder.cubic_to(hull[4], hull[2], to);
356 } else {
357 compute_hull(
360 hull[5],
361 hull[4],
362 hull[2],
363 to,
364 (end_t - start_t) / (1.0 - start_t),
365 &mut hull,
366 );
367
368 builder.cubic_to(hull[0], hull[3], hull[5]);
369 }
370 }
371 }
372 }
373 }
374}