1pub use crate::evolve_path::{evolve_path, evolve_path_with_length};
9
10pub fn approximate_path_length(path: &str) -> f64 {
20 let tokens = tokenize_path(path);
21 let mut idx = 0;
22 let mut total = 0.0;
23 let mut cx = 0.0f64;
24 let mut cy = 0.0f64;
25 let mut start_x = 0.0f64;
26 let mut start_y = 0.0f64;
27
28 while idx < tokens.len() {
29 let cmd = tokens[idx].as_str();
30 idx += 1;
31
32 match cmd {
33 "M" => {
34 let x = next_f64(&tokens, &mut idx);
35 let y = next_f64(&tokens, &mut idx);
36 cx = x;
37 cy = y;
38 start_x = cx;
39 start_y = cy;
40 while idx < tokens.len() && is_number(&tokens[idx]) {
41 let x2 = next_f64(&tokens, &mut idx);
42 let y2 = next_f64(&tokens, &mut idx);
43 total += dist(cx, cy, x2, y2);
44 cx = x2;
45 cy = y2;
46 }
47 }
48 "m" => {
49 let dx = next_f64(&tokens, &mut idx);
50 let dy = next_f64(&tokens, &mut idx);
51 cx += dx;
52 cy += dy;
53 start_x = cx;
54 start_y = cy;
55 while idx < tokens.len() && is_number(&tokens[idx]) {
56 let dx2 = next_f64(&tokens, &mut idx);
57 let dy2 = next_f64(&tokens, &mut idx);
58 total += dist(cx, cy, cx + dx2, cy + dy2);
59 cx += dx2;
60 cy += dy2;
61 }
62 }
63 "L" => {
64 while idx < tokens.len() && is_number(&tokens[idx]) {
65 let x = next_f64(&tokens, &mut idx);
66 let y = next_f64(&tokens, &mut idx);
67 total += dist(cx, cy, x, y);
68 cx = x;
69 cy = y;
70 }
71 }
72 "l" => {
73 while idx < tokens.len() && is_number(&tokens[idx]) {
74 let dx = next_f64(&tokens, &mut idx);
75 let dy = next_f64(&tokens, &mut idx);
76 total += dist(0.0, 0.0, dx, dy);
77 cx += dx;
78 cy += dy;
79 }
80 }
81 "H" => {
82 while idx < tokens.len() && is_number(&tokens[idx]) {
83 let x = next_f64(&tokens, &mut idx);
84 total += (x - cx).abs();
85 cx = x;
86 }
87 }
88 "h" => {
89 while idx < tokens.len() && is_number(&tokens[idx]) {
90 let dx = next_f64(&tokens, &mut idx);
91 total += dx.abs();
92 cx += dx;
93 }
94 }
95 "V" => {
96 while idx < tokens.len() && is_number(&tokens[idx]) {
97 let y = next_f64(&tokens, &mut idx);
98 total += (y - cy).abs();
99 cy = y;
100 }
101 }
102 "v" => {
103 while idx < tokens.len() && is_number(&tokens[idx]) {
104 let dy = next_f64(&tokens, &mut idx);
105 total += dy.abs();
106 cy += dy;
107 }
108 }
109 "C" => {
110 while idx < tokens.len() && is_number(&tokens[idx]) {
111 let x1 = next_f64(&tokens, &mut idx);
112 let y1 = next_f64(&tokens, &mut idx);
113 let x2 = next_f64(&tokens, &mut idx);
114 let y2 = next_f64(&tokens, &mut idx);
115 let x = next_f64(&tokens, &mut idx);
116 let y = next_f64(&tokens, &mut idx);
117 total += cubic_len((cx, cy), (x1, y1), (x2, y2), (x, y));
118 cx = x;
119 cy = y;
120 }
121 }
122 "c" => {
123 while idx < tokens.len() && is_number(&tokens[idx]) {
124 let dx1 = next_f64(&tokens, &mut idx);
125 let dy1 = next_f64(&tokens, &mut idx);
126 let dx2 = next_f64(&tokens, &mut idx);
127 let dy2 = next_f64(&tokens, &mut idx);
128 let dx = next_f64(&tokens, &mut idx);
129 let dy = next_f64(&tokens, &mut idx);
130 total += cubic_len(
131 (cx, cy),
132 (cx + dx1, cy + dy1),
133 (cx + dx2, cy + dy2),
134 (cx + dx, cy + dy),
135 );
136 cx += dx;
137 cy += dy;
138 }
139 }
140 "Q" => {
141 while idx < tokens.len() && is_number(&tokens[idx]) {
142 let x1 = next_f64(&tokens, &mut idx);
143 let y1 = next_f64(&tokens, &mut idx);
144 let x = next_f64(&tokens, &mut idx);
145 let y = next_f64(&tokens, &mut idx);
146 total += quad_len(cx, cy, x1, y1, x, y);
147 cx = x;
148 cy = y;
149 }
150 }
151 "q" => {
152 while idx < tokens.len() && is_number(&tokens[idx]) {
153 let dx1 = next_f64(&tokens, &mut idx);
154 let dy1 = next_f64(&tokens, &mut idx);
155 let dx = next_f64(&tokens, &mut idx);
156 let dy = next_f64(&tokens, &mut idx);
157 total += quad_len(cx, cy, cx + dx1, cy + dy1, cx + dx, cy + dy);
158 cx += dx;
159 cy += dy;
160 }
161 }
162 "A" => {
163 while idx < tokens.len() && is_number(&tokens[idx]) {
164 let rx = next_f64(&tokens, &mut idx);
165 let ry = next_f64(&tokens, &mut idx);
166 let x_axis_rotation = next_f64(&tokens, &mut idx);
167 let large_arc_flag = next_f64(&tokens, &mut idx) != 0.0;
168 let sweep_flag = next_f64(&tokens, &mut idx) != 0.0;
169 let x = next_f64(&tokens, &mut idx);
170 let y = next_f64(&tokens, &mut idx);
171 total += crate::length::arc_segment_length(
172 (cx, cy),
173 (x, y),
174 rx,
175 ry,
176 x_axis_rotation,
177 large_arc_flag,
178 sweep_flag,
179 );
180 cx = x;
181 cy = y;
182 }
183 }
184 "a" => {
185 while idx < tokens.len() && is_number(&tokens[idx]) {
186 let rx = next_f64(&tokens, &mut idx);
187 let ry = next_f64(&tokens, &mut idx);
188 let x_axis_rotation = next_f64(&tokens, &mut idx);
189 let large_arc_flag = next_f64(&tokens, &mut idx) != 0.0;
190 let sweep_flag = next_f64(&tokens, &mut idx) != 0.0;
191 let dx = next_f64(&tokens, &mut idx);
192 let dy = next_f64(&tokens, &mut idx);
193 let x = cx + dx;
194 let y = cy + dy;
195 total += crate::length::arc_segment_length(
196 (cx, cy),
197 (x, y),
198 rx,
199 ry,
200 x_axis_rotation,
201 large_arc_flag,
202 sweep_flag,
203 );
204 cx = x;
205 cy = y;
206 }
207 }
208 "Z" | "z" => {
209 total += dist(cx, cy, start_x, start_y);
210 cx = start_x;
211 cy = start_y;
212 }
213 _ => {}
214 }
215 }
216
217 total
218}
219
220pub fn interpolate_path(from: &str, to: &str, progress: f64) -> String {
230 let t = progress.clamp(0.0, 1.0);
231 let (from_nums, template) = extract_nums_and_template(from);
232 let (to_nums, _) = extract_nums_and_template(to);
233
234 if from_nums.len() != to_nums.len() {
235 return if t < 0.5 {
236 from.to_string()
237 } else {
238 to.to_string()
239 };
240 }
241
242 let interp: Vec<f64> = from_nums
243 .iter()
244 .zip(to_nums.iter())
245 .map(|(a, b)| a * (1.0 - t) + b * t)
246 .collect();
247
248 reconstruct(&template, &interp)
249}
250
251fn tokenize_path(d: &str) -> Vec<String> {
256 let mut tokens: Vec<String> = Vec::new();
257 let mut current = String::new();
258 for c in d.chars() {
259 if c.is_ascii_alphabetic() && c != 'e' && c != 'E' {
260 if !current.trim().is_empty() {
261 tokens.push(current.trim().to_string());
262 current.clear();
263 }
264 tokens.push(c.to_string());
265 } else if c == ',' || c.is_whitespace() {
266 if !current.trim().is_empty() {
267 tokens.push(current.trim().to_string());
268 current.clear();
269 }
270 } else if ((c == '-' || c == '+')
271 && !current.is_empty()
272 && !current.ends_with('e')
273 && !current.ends_with('E'))
274 || (c == '.' && current.contains('.'))
275 {
276 tokens.push(current.trim().to_string());
277 current.clear();
278 current.push(c);
279 } else {
280 current.push(c);
281 }
282 }
283 if !current.trim().is_empty() {
284 tokens.push(current.trim().to_string());
285 }
286 tokens
287}
288
289fn is_number(s: &str) -> bool {
290 s.parse::<f64>().is_ok()
291}
292
293fn next_f64(tokens: &[String], idx: &mut usize) -> f64 {
294 if *idx < tokens.len() {
295 let v = tokens[*idx].parse::<f64>().unwrap_or(0.0);
296 *idx += 1;
297 v
298 } else {
299 0.0
300 }
301}
302
303fn dist(x0: f64, y0: f64, x1: f64, y1: f64) -> f64 {
304 let dx = x1 - x0;
305 let dy = y1 - y0;
306 (dx * dx + dy * dy).sqrt()
307}
308
309fn cubic_len(
310 (x0, y0): (f64, f64),
311 (x1, y1): (f64, f64),
312 (x2, y2): (f64, f64),
313 (x3, y3): (f64, f64),
314) -> f64 {
315 const N: usize = 10;
316 let (mut len, mut px, mut py) = (0.0, x0, y0);
317 for i in 1..=N {
318 let t = i as f64 / N as f64;
319 let mt = 1.0 - t;
320 let qx =
321 mt * mt * mt * x0 + 3.0 * mt * mt * t * x1 + 3.0 * mt * t * t * x2 + t * t * t * x3;
322 let qy =
323 mt * mt * mt * y0 + 3.0 * mt * mt * t * y1 + 3.0 * mt * t * t * y2 + t * t * t * y3;
324 len += dist(px, py, qx, qy);
325 px = qx;
326 py = qy;
327 }
328 len
329}
330
331fn quad_len(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
332 const N: usize = 10;
333 let (mut len, mut px, mut py) = (0.0, x0, y0);
334 for i in 1..=N {
335 let t = i as f64 / N as f64;
336 let mt = 1.0 - t;
337 let qx = mt * mt * x0 + 2.0 * mt * t * x1 + t * t * x2;
338 let qy = mt * mt * y0 + 2.0 * mt * t * y1 + t * t * y2;
339 len += dist(px, py, qx, qy);
340 px = qx;
341 py = qy;
342 }
343 len
344}
345
346enum Segment {
351 Literal(String),
352 Number,
353}
354
355fn extract_nums_and_template(path: &str) -> (Vec<f64>, Vec<Segment>) {
356 let mut nums: Vec<f64> = Vec::new();
357 let mut template: Vec<Segment> = Vec::new();
358 let mut literal = String::new();
359 let chars: Vec<char> = path.chars().collect();
360 let mut i = 0;
361
362 while i < chars.len() {
363 let c = chars[i];
364 let is_num_start = c.is_ascii_digit()
365 || (c == '.' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit())
366 || (c == '-'
367 && i + 1 < chars.len()
368 && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.')
369 && (literal.is_empty()
371 || !literal.ends_with(|ch: char| ch.is_ascii_digit() || ch == '.')));
372 if is_num_start {
373 if !literal.is_empty() {
374 template.push(Segment::Literal(literal.clone()));
375 literal.clear();
376 }
377 let start = i;
378 if chars[i] == '-' {
379 i += 1;
380 }
381 while i < chars.len() && chars[i].is_ascii_digit() {
382 i += 1;
383 }
384 if i < chars.len() && chars[i] == '.' {
385 i += 1;
386 while i < chars.len() && chars[i].is_ascii_digit() {
387 i += 1;
388 }
389 }
390 if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
391 i += 1;
392 if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
393 i += 1;
394 }
395 while i < chars.len() && chars[i].is_ascii_digit() {
396 i += 1;
397 }
398 }
399 let num_str: String = chars[start..i].iter().collect();
400 if let Ok(v) = num_str.parse::<f64>() {
401 nums.push(v);
402 template.push(Segment::Number);
403 } else {
404 template.push(Segment::Literal(num_str));
405 }
406 } else {
407 literal.push(c);
408 i += 1;
409 }
410 }
411 if !literal.is_empty() {
412 template.push(Segment::Literal(literal));
413 }
414 (nums, template)
415}
416
417fn reconstruct(template: &[Segment], numbers: &[f64]) -> String {
418 let mut out = String::new();
419 let mut ni = 0;
420 for seg in template {
421 match seg {
422 Segment::Literal(s) => out.push_str(s),
423 Segment::Number => {
424 if ni < numbers.len() {
425 let v = numbers[ni];
426 if v.fract() == 0.0 && v.abs() < 1e10 {
427 out.push_str(&format!("{}", v as i64));
428 } else {
429 out.push_str(&format!("{:.4}", v));
430 }
431 ni += 1;
432 }
433 }
434 }
435 }
436 out
437}
438
439#[cfg(test)]
443mod tests {
444 use super::*;
445 use std::f64::consts::PI;
446
447 #[test]
448 fn approximate_path_length_line() {
449 let len = approximate_path_length("M 0 0 L 3 4");
451 assert!((len - 5.0).abs() < 0.01, "expected ~5, got {len}");
452 }
453
454 #[test]
455 fn approximate_path_length_rect() {
456 let len = approximate_path_length("M 0 0 L 100 0 L 100 100 L 0 100 Z");
457 assert!((len - 400.0).abs() < 0.1, "expected ~400, got {len}");
458 }
459
460 #[test]
461 fn approximate_path_length_h_v() {
462 let len = approximate_path_length("M 0 0 H 50 V 50");
463 assert!((len - 100.0).abs() < 0.01, "expected ~100, got {len}");
464 }
465
466 #[test]
467 fn approximate_path_length_arc() {
468 let len = approximate_path_length("M 100 0 A 100 100 0 0 1 0 100");
469 let expected = 0.5 * PI * 100.0;
470 assert!(
471 (len - expected).abs() < 0.5,
472 "expected ~{expected}, got {len}"
473 );
474 }
475
476 #[test]
477 fn interpolate_path_midpoint() {
478 let from = "M 0 0 L 100 0";
479 let to = "M 0 0 L 0 100";
480 let mid = interpolate_path(from, to, 0.5);
481 assert!(mid.contains("50"), "mid path should contain 50: {mid}");
482 }
483
484 #[test]
485 fn interpolate_path_incompatible_falls_back() {
486 let from = "M 0 0 L 10 0";
487 let to = "M 0 0 L 10 0 L 20 0";
488 let r_lo = interpolate_path(from, to, 0.3);
489 let r_hi = interpolate_path(from, to, 0.7);
490 assert_eq!(r_lo, from);
491 assert_eq!(r_hi, to);
492 }
493
494 #[test]
495 fn interpolate_path_endpoints() {
496 let from = "M 10 20 L 30 40";
497 let to = "M 50 60 L 70 80";
498 assert_eq!(interpolate_path(from, to, 0.0), from);
499 assert_eq!(interpolate_path(from, to, 1.0), to);
500 }
501
502 #[test]
503 fn interpolate_path_beziers_and_negative() {
504 let from = "M -10 -20 C 0 10 20 30 40 50";
505 let to = "M 10 20 C 20 30 40 50 60 70";
506 let mid = interpolate_path(from, to, 0.5);
507 assert_eq!(mid, "M 0 0 C 10 20 30 40 50 60");
508 }
509
510 #[test]
511 fn approximate_path_length_parity_with_get_length() {
512 let path = "M 0 0 L 100 0 L 100 100 A 50 50 0 0 1 50 150 Z";
513 let approx = approximate_path_length(path);
514 let actual = crate::length::get_length(path);
515 assert!(
516 (approx - actual).abs() < 1.0,
517 "approx {approx} vs actual {actual}"
518 );
519 }
520}