1use crate::parser::{parse_path, serialize_instructions};
4use crate::types::{Instruction, Point};
5
6pub fn translate_path(path: &str, dx: f64, dy: f64) -> String {
8 let mut instructions = match parse_path(path) {
9 Ok(insts) => insts,
10 Err(_) => return path.to_string(),
11 };
12
13 for inst in &mut instructions {
14 match inst {
15 Instruction::MoveTo { x, y } => {
16 *x += dx;
17 *y += dy;
18 }
19 Instruction::LineTo { x, y } => {
20 *x += dx;
21 *y += dy;
22 }
23 Instruction::CubicCurveTo {
24 x1,
25 y1,
26 x2,
27 y2,
28 x,
29 y,
30 } => {
31 *x1 += dx;
32 *y1 += dy;
33 *x2 += dx;
34 *y2 += dy;
35 *x += dx;
36 *y += dy;
37 }
38 Instruction::QuadCurveTo { x1, y1, x, y } => {
39 *x1 += dx;
40 *y1 += dy;
41 *x += dx;
42 *y += dy;
43 }
44 Instruction::ArcTo { x, y, .. } => {
45 *x += dx;
46 *y += dy;
47 }
48 Instruction::ClosePath => {}
49 }
50 }
51
52 serialize_instructions(&instructions)
53}
54
55pub fn scale_path(path: &str, sx: f64, sy: f64) -> String {
57 let mut instructions = match parse_path(path) {
58 Ok(insts) => insts,
59 Err(_) => return path.to_string(),
60 };
61
62 for inst in &mut instructions {
63 match inst {
64 Instruction::MoveTo { x, y } => {
65 *x *= sx;
66 *y *= sy;
67 }
68 Instruction::LineTo { x, y } => {
69 *x *= sx;
70 *y *= sy;
71 }
72 Instruction::CubicCurveTo {
73 x1,
74 y1,
75 x2,
76 y2,
77 x,
78 y,
79 } => {
80 *x1 *= sx;
81 *y1 *= sy;
82 *x2 *= sx;
83 *y2 *= sy;
84 *x *= sx;
85 *y *= sy;
86 }
87 Instruction::QuadCurveTo { x1, y1, x, y } => {
88 *x1 *= sx;
89 *y1 *= sy;
90 *x *= sx;
91 *y *= sy;
92 }
93 Instruction::ArcTo { rx, ry, x, y, .. } => {
94 *rx *= sx.abs();
95 *ry *= sy.abs();
96 *x *= sx;
97 *y *= sy;
98 }
99 Instruction::ClosePath => {}
100 }
101 }
102
103 serialize_instructions(&instructions)
104}
105
106pub fn rotate_path(path: &str, angle_rad: f64, cx: f64, cy: f64) -> String {
108 let mut instructions = match parse_path(path) {
109 Ok(insts) => insts,
110 Err(_) => return path.to_string(),
111 };
112 let cos = angle_rad.cos();
113 let sin = angle_rad.sin();
114 let rotate_pt = |x: &mut f64, y: &mut f64| {
115 let dx = *x - cx;
116 let dy = *y - cy;
117 *x = cx + dx * cos - dy * sin;
118 *y = cy + dx * sin + dy * cos;
119 };
120
121 for inst in &mut instructions {
122 match inst {
123 Instruction::MoveTo { x, y } | Instruction::LineTo { x, y } => {
124 rotate_pt(x, y);
125 }
126 Instruction::CubicCurveTo {
127 x1,
128 y1,
129 x2,
130 y2,
131 x,
132 y,
133 } => {
134 rotate_pt(x1, y1);
135 rotate_pt(x2, y2);
136 rotate_pt(x, y);
137 }
138 Instruction::QuadCurveTo { x1, y1, x, y } => {
139 rotate_pt(x1, y1);
140 rotate_pt(x, y);
141 }
142 Instruction::ArcTo {
143 x,
144 y,
145 x_axis_rotation,
146 ..
147 } => {
148 rotate_pt(x, y);
149 *x_axis_rotation = (*x_axis_rotation + angle_rad.to_degrees()).rem_euclid(360.0);
150 }
151 Instruction::ClosePath => {}
152 }
153 }
154
155 serialize_instructions(&instructions)
156}
157
158pub fn reverse_path(path: &str) -> String {
160 let instructions = match parse_path(path) {
161 Ok(insts) => insts,
162 Err(_) => return path.to_string(),
163 };
164
165 if instructions.is_empty() {
166 return path.to_string();
167 }
168
169 let mut subpaths: Vec<Vec<(Point, Instruction)>> = Vec::new();
171 let mut current_subpath: Vec<(Point, Instruction)> = Vec::new();
172 let mut current_pt = Point::new(0.0, 0.0);
173 let mut start_pt = Point::new(0.0, 0.0);
174
175 for inst in instructions {
176 match inst {
177 Instruction::MoveTo { x, y } => {
178 if !current_subpath.is_empty() {
179 subpaths.push(current_subpath);
180 current_subpath = Vec::new();
181 }
182 current_pt = Point::new(x, y);
183 start_pt = current_pt;
184 current_subpath.push((current_pt, inst));
185 }
186 Instruction::LineTo { x, y } => {
187 current_subpath.push((current_pt, inst));
188 current_pt = Point::new(x, y);
189 }
190 Instruction::CubicCurveTo { x, y, .. } => {
191 current_subpath.push((current_pt, inst));
192 current_pt = Point::new(x, y);
193 }
194 Instruction::QuadCurveTo { x, y, .. } => {
195 current_subpath.push((current_pt, inst));
196 current_pt = Point::new(x, y);
197 }
198 Instruction::ArcTo { x, y, .. } => {
199 current_subpath.push((current_pt, inst));
200 current_pt = Point::new(x, y);
201 }
202 Instruction::ClosePath => {
203 current_subpath.push((current_pt, inst));
204 current_pt = start_pt;
205 }
206 }
207 }
208 if !current_subpath.is_empty() {
209 subpaths.push(current_subpath);
210 }
211
212 let mut reversed_instructions = Vec::new();
213
214 for subpath in subpaths {
215 if subpath.is_empty() {
216 continue;
217 }
218
219 let has_close = matches!(subpath.last(), Some((_, Instruction::ClosePath)));
220 let end_idx = if has_close {
221 subpath.len() - 1
222 } else {
223 subpath.len()
224 };
225 let segments = &subpath[..end_idx];
226
227 if segments.is_empty() {
228 continue;
229 }
230
231 let last_target = match segments.last().unwrap().1 {
233 Instruction::MoveTo { x, y }
234 | Instruction::LineTo { x, y }
235 | Instruction::CubicCurveTo { x, y, .. }
236 | Instruction::QuadCurveTo { x, y, .. }
237 | Instruction::ArcTo { x, y, .. } => Point::new(x, y),
238 Instruction::ClosePath => segments[0].0,
239 };
240
241 reversed_instructions.push(Instruction::MoveTo {
242 x: last_target.x,
243 y: last_target.y,
244 });
245
246 for (prev_pt, inst) in segments.iter().skip(1).rev() {
248 match *inst {
249 Instruction::MoveTo { .. } => {}
250 Instruction::LineTo { .. } => {
251 reversed_instructions.push(Instruction::LineTo {
252 x: prev_pt.x,
253 y: prev_pt.y,
254 });
255 }
256 Instruction::CubicCurveTo { x1, y1, x2, y2, .. } => {
257 reversed_instructions.push(Instruction::CubicCurveTo {
258 x1: x2,
259 y1: y2,
260 x2: x1,
261 y2: y1,
262 x: prev_pt.x,
263 y: prev_pt.y,
264 });
265 }
266 Instruction::QuadCurveTo { x1, y1, .. } => {
267 reversed_instructions.push(Instruction::QuadCurveTo {
268 x1,
269 y1,
270 x: prev_pt.x,
271 y: prev_pt.y,
272 });
273 }
274 Instruction::ArcTo {
275 rx,
276 ry,
277 x_axis_rotation,
278 large_arc_flag,
279 sweep_flag,
280 ..
281 } => {
282 reversed_instructions.push(Instruction::ArcTo {
283 rx,
284 ry,
285 x_axis_rotation,
286 large_arc_flag,
287 sweep_flag: !sweep_flag,
288 x: prev_pt.x,
289 y: prev_pt.y,
290 });
291 }
292 Instruction::ClosePath => {}
293 }
294 }
295
296 if has_close {
297 reversed_instructions.push(Instruction::ClosePath);
298 }
299 }
300
301 serialize_instructions(&reversed_instructions)
302}
303
304pub fn reset_path(path: &str) -> String {
306 if let Some(bbox) = crate::bounding_box::get_bounding_box(path) {
307 translate_path(path, -bbox.x, -bbox.y)
308 } else {
309 path.to_string()
310 }
311}
312
313pub fn warp_path<F>(path: &str, mut f: F) -> String
315where
316 F: FnMut(f64, f64) -> (f64, f64),
317{
318 let mut instructions = match parse_path(path) {
319 Ok(insts) => insts,
320 Err(_) => return path.to_string(),
321 };
322
323 for inst in &mut instructions {
324 match inst {
325 Instruction::MoveTo { x, y } | Instruction::LineTo { x, y } => {
326 let (nx, ny) = f(*x, *y);
327 *x = nx;
328 *y = ny;
329 }
330 Instruction::CubicCurveTo {
331 x1,
332 y1,
333 x2,
334 y2,
335 x,
336 y,
337 } => {
338 let (nx1, ny1) = f(*x1, *y1);
339 let (nx2, ny2) = f(*x2, *y2);
340 let (nx, ny) = f(*x, *y);
341 *x1 = nx1;
342 *y1 = ny1;
343 *x2 = nx2;
344 *y2 = ny2;
345 *x = nx;
346 *y = ny;
347 }
348 Instruction::QuadCurveTo { x1, y1, x, y } => {
349 let (nx1, ny1) = f(*x1, *y1);
350 let (nx, ny) = f(*x, *y);
351 *x1 = nx1;
352 *y1 = ny1;
353 *x = nx;
354 *y = ny;
355 }
356 Instruction::ArcTo { x, y, .. } => {
357 let (nx, ny) = f(*x, *y);
358 *x = nx;
359 *y = ny;
360 }
361 Instruction::ClosePath => {}
362 }
363 }
364
365 serialize_instructions(&instructions)
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_translate_path() {
374 let path = "M 0 0 L 10 10 Z";
375 let res = translate_path(path, 5.0, 15.0);
376 assert_eq!(res, "M 5.0000 15.0000 L 15.0000 25.0000 Z");
377 }
378
379 #[test]
380 fn test_scale_path() {
381 let path = "M 10 20 L 30 40 Z";
382 let res = scale_path(path, 2.0, 0.5);
383 assert_eq!(res, "M 20.0000 10.0000 L 60.0000 20.0000 Z");
384 }
385
386 #[test]
387 fn test_rotate_path() {
388 let path = "M 10 0 L 20 0";
389 let rotated = rotate_path(path, std::f64::consts::FRAC_PI_2, 0.0, 0.0);
391 let insts = parse_path(&rotated).unwrap();
392 match &insts[0] {
393 Instruction::MoveTo { x, y } => {
394 assert!(x.abs() < 1e-4);
395 assert!((y - 10.0).abs() < 1e-4);
396 }
397 _ => panic!("expected MoveTo"),
398 }
399 }
400
401 #[test]
402 fn test_reverse_path() {
403 let path = "M 0 0 L 10 20 L 30 40";
404 let rev = reverse_path(path);
405 let insts = parse_path(&rev).unwrap();
406 assert_eq!(insts[0], Instruction::MoveTo { x: 30.0, y: 40.0 });
407 assert_eq!(insts[1], Instruction::LineTo { x: 10.0, y: 20.0 });
408 assert_eq!(insts[2], Instruction::LineTo { x: 0.0, y: 0.0 });
409 }
410
411 #[test]
412 fn test_reset_path() {
413 let path = "M 15 25 L 35 45";
414 let reset = reset_path(path);
415 let insts = parse_path(&reset).unwrap();
416 assert_eq!(insts[0], Instruction::MoveTo { x: 0.0, y: 0.0 });
417 assert_eq!(insts[1], Instruction::LineTo { x: 20.0, y: 20.0 });
418 }
419
420 #[test]
421 fn test_warp_path() {
422 let path = "M 10 10 L 20 20";
423 let warped = warp_path(path, |x, y| (x * 2.0, y + 5.0));
424 let insts = parse_path(&warped).unwrap();
425 assert_eq!(insts[0], Instruction::MoveTo { x: 20.0, y: 15.0 });
426 assert_eq!(insts[1], Instruction::LineTo { x: 40.0, y: 25.0 });
427 }
428
429 #[test]
430 fn test_translate_and_scale_arc() {
431 let path = "M 0 0 A 10 20 0 0 1 10 20";
432 let translated = translate_path(path, 5.0, 5.0);
433 assert_eq!(
434 translated,
435 "M 5.0000 5.0000 A 10.0000 20.0000 0.0000 0 1 15.0000 25.0000"
436 );
437
438 let scaled = scale_path(path, 2.0, 3.0);
439 assert_eq!(
440 scaled,
441 "M 0.0000 0.0000 A 20.0000 60.0000 0.0000 0 1 20.0000 60.0000"
442 );
443 }
444
445 #[test]
446 fn test_transform_invalid_path_returns_original() {
447 let invalid = "not a valid path";
448 assert_eq!(translate_path(invalid, 10.0, 10.0), invalid);
449 assert_eq!(scale_path(invalid, 2.0, 2.0), invalid);
450 }
451}