1 | // Copyright 2018 the Kurbo Authors
|
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT
|
3 |
|
4 | //! SVG path representation.
|
5 |
|
6 | use std::error::Error;
|
7 | use std::f64::consts::PI;
|
8 | use std::fmt::{self, Display, Formatter};
|
9 | use std::io::{self, Write};
|
10 |
|
11 | use crate::{Arc, BezPath, ParamCurve, PathEl, PathSeg, Point, Vec2};
|
12 |
|
13 | // Note: the SVG arc logic is heavily adapted from https://github.com/nical/lyon
|
14 |
|
15 | /// A single SVG arc segment.
|
16 | #[derive (Clone, Copy, Debug)]
|
17 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))]
|
18 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))]
|
19 | pub struct SvgArc {
|
20 | /// The arc's start point.
|
21 | pub from: Point,
|
22 | /// The arc's end point.
|
23 | pub to: Point,
|
24 | /// The arc's radii, where the vector's x-component is the radius in the
|
25 | /// positive x direction after applying `x_rotation`.
|
26 | pub radii: Vec2,
|
27 | /// How much the arc is rotated, in radians.
|
28 | pub x_rotation: f64,
|
29 | /// Does this arc sweep through more than π radians?
|
30 | pub large_arc: bool,
|
31 | /// Determines if the arc should begin moving at positive angles.
|
32 | pub sweep: bool,
|
33 | }
|
34 |
|
35 | impl BezPath {
|
36 | /// Create a BezPath with segments corresponding to the sequence of
|
37 | /// `PathSeg`s
|
38 | pub fn from_path_segments(segments: impl Iterator<Item = PathSeg>) -> BezPath {
|
39 | let mut path_elements = Vec::new();
|
40 | let mut current_pos = None;
|
41 |
|
42 | for segment in segments {
|
43 | let start = segment.start();
|
44 | if Some(start) != current_pos {
|
45 | path_elements.push(PathEl::MoveTo(start));
|
46 | };
|
47 | path_elements.push(match segment {
|
48 | PathSeg::Line(l) => PathEl::LineTo(l.p1),
|
49 | PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2),
|
50 | PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3),
|
51 | });
|
52 |
|
53 | current_pos = Some(segment.end());
|
54 | }
|
55 |
|
56 | BezPath::from_vec(path_elements)
|
57 | }
|
58 |
|
59 | /// Convert the path to an SVG path string representation.
|
60 | ///
|
61 | /// The current implementation doesn't take any special care to produce a
|
62 | /// short string (reducing precision, using relative movement).
|
63 | pub fn to_svg(&self) -> String {
|
64 | let mut buffer = Vec::new();
|
65 | self.write_to(&mut buffer).unwrap();
|
66 | String::from_utf8(buffer).unwrap()
|
67 | }
|
68 |
|
69 | /// Write the SVG representation of this path to the provided buffer.
|
70 | pub fn write_to<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
71 | for (i, el) in self.elements().iter().enumerate() {
|
72 | if i > 0 {
|
73 | write!(writer, " " )?;
|
74 | }
|
75 | match *el {
|
76 | PathEl::MoveTo(p) => write!(writer, "M {}, {}" , p.x, p.y)?,
|
77 | PathEl::LineTo(p) => write!(writer, "L {}, {}" , p.x, p.y)?,
|
78 | PathEl::QuadTo(p1, p2) => write!(writer, "Q {}, {} {}, {}" , p1.x, p1.y, p2.x, p2.y)?,
|
79 | PathEl::CurveTo(p1, p2, p3) => write!(
|
80 | writer,
|
81 | "C {}, {} {}, {} {}, {}" ,
|
82 | p1.x, p1.y, p2.x, p2.y, p3.x, p3.y
|
83 | )?,
|
84 | PathEl::ClosePath => write!(writer, "Z" )?,
|
85 | }
|
86 | }
|
87 |
|
88 | Ok(())
|
89 | }
|
90 |
|
91 | /// Try to parse a bezier path from an SVG path element.
|
92 | ///
|
93 | /// This is implemented on a best-effort basis, intended for cases where the
|
94 | /// user controls the source of paths, and is not intended as a replacement
|
95 | /// for a general, robust SVG parser.
|
96 | pub fn from_svg(data: &str) -> Result<BezPath, SvgParseError> {
|
97 | let mut lexer = SvgLexer::new(data);
|
98 | let mut path = BezPath::new();
|
99 | let mut last_cmd = 0;
|
100 | let mut last_ctrl = None;
|
101 | let mut first_pt = Point::ORIGIN;
|
102 | let mut implicit_moveto = None;
|
103 | while let Some(c) = lexer.get_cmd(last_cmd) {
|
104 | if c != b'm' && c != b'M' {
|
105 | if let Some(pt) = implicit_moveto.take() {
|
106 | path.move_to(pt);
|
107 | }
|
108 | }
|
109 | match c {
|
110 | b'm' | b'M' => {
|
111 | implicit_moveto = None;
|
112 | let pt = lexer.get_maybe_relative(c)?;
|
113 | path.move_to(pt);
|
114 | lexer.last_pt = pt;
|
115 | first_pt = pt;
|
116 | last_ctrl = Some(pt);
|
117 | last_cmd = c - (b'M' - b'L' );
|
118 | }
|
119 | b'l' | b'L' => {
|
120 | let pt = lexer.get_maybe_relative(c)?;
|
121 | path.line_to(pt);
|
122 | lexer.last_pt = pt;
|
123 | last_ctrl = Some(pt);
|
124 | last_cmd = c;
|
125 | }
|
126 | b'h' | b'H' => {
|
127 | let mut x = lexer.get_number()?;
|
128 | lexer.opt_comma();
|
129 | if c == b'h' {
|
130 | x += lexer.last_pt.x;
|
131 | }
|
132 | let pt = Point::new(x, lexer.last_pt.y);
|
133 | path.line_to(pt);
|
134 | lexer.last_pt = pt;
|
135 | last_ctrl = Some(pt);
|
136 | last_cmd = c;
|
137 | }
|
138 | b'v' | b'V' => {
|
139 | let mut y = lexer.get_number()?;
|
140 | lexer.opt_comma();
|
141 | if c == b'v' {
|
142 | y += lexer.last_pt.y;
|
143 | }
|
144 | let pt = Point::new(lexer.last_pt.x, y);
|
145 | path.line_to(pt);
|
146 | lexer.last_pt = pt;
|
147 | last_ctrl = Some(pt);
|
148 | last_cmd = c;
|
149 | }
|
150 | b'q' | b'Q' => {
|
151 | let p1 = lexer.get_maybe_relative(c)?;
|
152 | let p2 = lexer.get_maybe_relative(c)?;
|
153 | path.quad_to(p1, p2);
|
154 | last_ctrl = Some(p1);
|
155 | lexer.last_pt = p2;
|
156 | last_cmd = c;
|
157 | }
|
158 | b't' | b'T' => {
|
159 | let p1 = match last_ctrl {
|
160 | Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(),
|
161 | None => lexer.last_pt,
|
162 | };
|
163 | let p2 = lexer.get_maybe_relative(c)?;
|
164 | path.quad_to(p1, p2);
|
165 | last_ctrl = Some(p1);
|
166 | lexer.last_pt = p2;
|
167 | last_cmd = c;
|
168 | }
|
169 | b'c' | b'C' => {
|
170 | let p1 = lexer.get_maybe_relative(c)?;
|
171 | let p2 = lexer.get_maybe_relative(c)?;
|
172 | let p3 = lexer.get_maybe_relative(c)?;
|
173 | path.curve_to(p1, p2, p3);
|
174 | last_ctrl = Some(p2);
|
175 | lexer.last_pt = p3;
|
176 | last_cmd = c;
|
177 | }
|
178 | b's' | b'S' => {
|
179 | let p1 = match last_ctrl {
|
180 | Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(),
|
181 | None => lexer.last_pt,
|
182 | };
|
183 | let p2 = lexer.get_maybe_relative(c)?;
|
184 | let p3 = lexer.get_maybe_relative(c)?;
|
185 | path.curve_to(p1, p2, p3);
|
186 | last_ctrl = Some(p2);
|
187 | lexer.last_pt = p3;
|
188 | last_cmd = c;
|
189 | }
|
190 | b'a' | b'A' => {
|
191 | let radii = lexer.get_number_pair()?;
|
192 | let x_rotation = lexer.get_number()?.to_radians();
|
193 | lexer.opt_comma();
|
194 | let large_arc = lexer.get_flag()?;
|
195 | lexer.opt_comma();
|
196 | let sweep = lexer.get_flag()?;
|
197 | lexer.opt_comma();
|
198 | let p = lexer.get_maybe_relative(c)?;
|
199 | let svg_arc = SvgArc {
|
200 | from: lexer.last_pt,
|
201 | to: p,
|
202 | radii: radii.to_vec2(),
|
203 | x_rotation,
|
204 | large_arc,
|
205 | sweep,
|
206 | };
|
207 |
|
208 | match Arc::from_svg_arc(&svg_arc) {
|
209 | Some(arc) => {
|
210 | // TODO: consider making tolerance configurable
|
211 | arc.to_cubic_beziers(0.1, |p1, p2, p3| {
|
212 | path.curve_to(p1, p2, p3);
|
213 | });
|
214 | }
|
215 | None => {
|
216 | path.line_to(p);
|
217 | }
|
218 | }
|
219 |
|
220 | last_ctrl = Some(p);
|
221 | lexer.last_pt = p;
|
222 | last_cmd = c;
|
223 | }
|
224 | b'z' | b'Z' => {
|
225 | path.close_path();
|
226 | lexer.last_pt = first_pt;
|
227 | implicit_moveto = Some(first_pt);
|
228 | }
|
229 | _ => return Err(SvgParseError::UnknownCommand(c as char)),
|
230 | }
|
231 | }
|
232 | Ok(path)
|
233 | }
|
234 | }
|
235 |
|
236 | /// An error which can be returned when parsing an SVG.
|
237 | #[derive (Debug)]
|
238 | pub enum SvgParseError {
|
239 | /// A number was expected.
|
240 | Wrong,
|
241 | /// The input string ended while still expecting input.
|
242 | UnexpectedEof,
|
243 | /// Encountered an unknown command letter.
|
244 | UnknownCommand(char),
|
245 | }
|
246 |
|
247 | impl Display for SvgParseError {
|
248 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
249 | match self {
|
250 | SvgParseError::Wrong => write!(f, "Unable to parse a number" ),
|
251 | SvgParseError::UnexpectedEof => write!(f, "Unexpected EOF" ),
|
252 | SvgParseError::UnknownCommand(letter: &char) => write!(f, "Unknown command, \"{letter}\"" ),
|
253 | }
|
254 | }
|
255 | }
|
256 |
|
257 | impl Error for SvgParseError {}
|
258 |
|
259 | struct SvgLexer<'a> {
|
260 | data: &'a str,
|
261 | ix: usize,
|
262 | pub last_pt: Point,
|
263 | }
|
264 |
|
265 | impl<'a> SvgLexer<'a> {
|
266 | fn new(data: &str) -> SvgLexer {
|
267 | SvgLexer {
|
268 | data,
|
269 | ix: 0,
|
270 | last_pt: Point::ORIGIN,
|
271 | }
|
272 | }
|
273 |
|
274 | fn skip_ws(&mut self) {
|
275 | while let Some(&c) = self.data.as_bytes().get(self.ix) {
|
276 | if !(c == b' ' || c == 9 || c == 10 || c == 12 || c == 13) {
|
277 | break;
|
278 | }
|
279 | self.ix += 1;
|
280 | }
|
281 | }
|
282 |
|
283 | fn get_cmd(&mut self, last_cmd: u8) -> Option<u8> {
|
284 | self.skip_ws();
|
285 | if let Some(c) = self.get_byte() {
|
286 | if c.is_ascii_lowercase() || c.is_ascii_uppercase() {
|
287 | return Some(c);
|
288 | } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) {
|
289 | // Plausible number start
|
290 | self.unget();
|
291 | return Some(last_cmd);
|
292 | } else {
|
293 | self.unget();
|
294 | }
|
295 | }
|
296 | None
|
297 | }
|
298 |
|
299 | fn get_byte(&mut self) -> Option<u8> {
|
300 | self.data.as_bytes().get(self.ix).map(|&c| {
|
301 | self.ix += 1;
|
302 | c
|
303 | })
|
304 | }
|
305 |
|
306 | fn unget(&mut self) {
|
307 | self.ix -= 1;
|
308 | }
|
309 |
|
310 | fn get_number(&mut self) -> Result<f64, SvgParseError> {
|
311 | self.skip_ws();
|
312 | let start = self.ix;
|
313 | let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?;
|
314 | if !(c == b'-' || c == b'+' ) {
|
315 | self.unget();
|
316 | }
|
317 | let mut digit_count = 0;
|
318 | let mut seen_period = false;
|
319 | while let Some(c) = self.get_byte() {
|
320 | if c.is_ascii_digit() {
|
321 | digit_count += 1;
|
322 | } else if c == b'.' && !seen_period {
|
323 | seen_period = true;
|
324 | } else {
|
325 | self.unget();
|
326 | break;
|
327 | }
|
328 | }
|
329 | if let Some(c) = self.get_byte() {
|
330 | if c == b'e' || c == b'E' {
|
331 | let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?;
|
332 | if c == b'-' || c == b'+' {
|
333 | c = self.get_byte().ok_or(SvgParseError::Wrong)?
|
334 | }
|
335 | if c.is_ascii_digit() {
|
336 | return Err(SvgParseError::Wrong);
|
337 | }
|
338 | while let Some(c) = self.get_byte() {
|
339 | if c.is_ascii_digit() {
|
340 | self.unget();
|
341 | break;
|
342 | }
|
343 | }
|
344 | } else {
|
345 | self.unget();
|
346 | }
|
347 | }
|
348 | if digit_count > 0 {
|
349 | self.data[start..self.ix]
|
350 | .parse()
|
351 | .map_err(|_| SvgParseError::Wrong)
|
352 | } else {
|
353 | Err(SvgParseError::Wrong)
|
354 | }
|
355 | }
|
356 |
|
357 | fn get_flag(&mut self) -> Result<bool, SvgParseError> {
|
358 | self.skip_ws();
|
359 | match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? {
|
360 | b'0' => Ok(false),
|
361 | b'1' => Ok(true),
|
362 | _ => Err(SvgParseError::Wrong),
|
363 | }
|
364 | }
|
365 |
|
366 | fn get_number_pair(&mut self) -> Result<Point, SvgParseError> {
|
367 | let x = self.get_number()?;
|
368 | self.opt_comma();
|
369 | let y = self.get_number()?;
|
370 | self.opt_comma();
|
371 | Ok(Point::new(x, y))
|
372 | }
|
373 |
|
374 | fn get_maybe_relative(&mut self, cmd: u8) -> Result<Point, SvgParseError> {
|
375 | let pt = self.get_number_pair()?;
|
376 | if cmd.is_ascii_lowercase() {
|
377 | Ok(self.last_pt + pt.to_vec2())
|
378 | } else {
|
379 | Ok(pt)
|
380 | }
|
381 | }
|
382 |
|
383 | fn opt_comma(&mut self) {
|
384 | self.skip_ws();
|
385 | if let Some(c) = self.get_byte() {
|
386 | if c != b',' {
|
387 | self.unget();
|
388 | }
|
389 | }
|
390 | }
|
391 | }
|
392 |
|
393 | impl SvgArc {
|
394 | /// Checks that arc is actually a straight line.
|
395 | ///
|
396 | /// In this case, it can be replaced with a LineTo.
|
397 | pub fn is_straight_line(&self) -> bool {
|
398 | self.radii.x.abs() <= 1e-5 || self.radii.y.abs() <= 1e-5 || self.from == self.to
|
399 | }
|
400 | }
|
401 |
|
402 | impl Arc {
|
403 | /// Creates an `Arc` from a `SvgArc`.
|
404 | ///
|
405 | /// Returns `None` if `arc` is actually a straight line.
|
406 | pub fn from_svg_arc(arc: &SvgArc) -> Option<Arc> {
|
407 | // Have to check this first, otherwise `sum_of_sq` will be 0.
|
408 | if arc.is_straight_line() {
|
409 | return None;
|
410 | }
|
411 |
|
412 | let mut rx = arc.radii.x.abs();
|
413 | let mut ry = arc.radii.y.abs();
|
414 |
|
415 | let xr = arc.x_rotation % (2.0 * PI);
|
416 | let (sin_phi, cos_phi) = xr.sin_cos();
|
417 | let hd_x = (arc.from.x - arc.to.x) * 0.5;
|
418 | let hd_y = (arc.from.y - arc.to.y) * 0.5;
|
419 | let hs_x = (arc.from.x + arc.to.x) * 0.5;
|
420 | let hs_y = (arc.from.y + arc.to.y) * 0.5;
|
421 |
|
422 | // F6.5.1
|
423 | let p = Vec2::new(
|
424 | cos_phi * hd_x + sin_phi * hd_y,
|
425 | -sin_phi * hd_x + cos_phi * hd_y,
|
426 | );
|
427 |
|
428 | // Sanitize the radii.
|
429 | // If rf > 1 it means the radii are too small for the arc to
|
430 | // possibly connect the end points. In this situation we scale
|
431 | // them up according to the formula provided by the SVG spec.
|
432 |
|
433 | // F6.6.2
|
434 | let rf = p.x * p.x / (rx * rx) + p.y * p.y / (ry * ry);
|
435 | if rf > 1.0 {
|
436 | let scale = rf.sqrt();
|
437 | rx *= scale;
|
438 | ry *= scale;
|
439 | }
|
440 |
|
441 | let rxry = rx * ry;
|
442 | let rxpy = rx * p.y;
|
443 | let rypx = ry * p.x;
|
444 | let sum_of_sq = rxpy * rxpy + rypx * rypx;
|
445 |
|
446 | debug_assert!(sum_of_sq != 0.0);
|
447 |
|
448 | // F6.5.2
|
449 | let sign_coe = if arc.large_arc == arc.sweep {
|
450 | -1.0
|
451 | } else {
|
452 | 1.0
|
453 | };
|
454 | let coe = sign_coe * ((rxry * rxry - sum_of_sq) / sum_of_sq).abs().sqrt();
|
455 | let transformed_cx = coe * rxpy / ry;
|
456 | let transformed_cy = -coe * rypx / rx;
|
457 |
|
458 | // F6.5.3
|
459 | let center = Point::new(
|
460 | cos_phi * transformed_cx - sin_phi * transformed_cy + hs_x,
|
461 | sin_phi * transformed_cx + cos_phi * transformed_cy + hs_y,
|
462 | );
|
463 |
|
464 | let start_v = Vec2::new((p.x - transformed_cx) / rx, (p.y - transformed_cy) / ry);
|
465 | let end_v = Vec2::new((-p.x - transformed_cx) / rx, (-p.y - transformed_cy) / ry);
|
466 |
|
467 | let start_angle = start_v.atan2();
|
468 |
|
469 | let mut sweep_angle = (end_v.atan2() - start_angle) % (2.0 * PI);
|
470 |
|
471 | if arc.sweep && sweep_angle < 0.0 {
|
472 | sweep_angle += 2.0 * PI;
|
473 | } else if !arc.sweep && sweep_angle > 0.0 {
|
474 | sweep_angle -= 2.0 * PI;
|
475 | }
|
476 |
|
477 | Some(Arc {
|
478 | center,
|
479 | radii: Vec2::new(rx, ry),
|
480 | start_angle,
|
481 | sweep_angle,
|
482 | x_rotation: arc.x_rotation,
|
483 | })
|
484 | }
|
485 | }
|
486 |
|
487 | #[cfg (test)]
|
488 | mod tests {
|
489 | use crate::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Shape};
|
490 |
|
491 | #[test ]
|
492 | fn test_parse_svg() {
|
493 | let path = BezPath::from_svg("m10 10 100 0 0 100 -100 0z" ).unwrap();
|
494 | assert_eq!(path.segments().count(), 4);
|
495 | }
|
496 |
|
497 | #[test ]
|
498 | fn test_parse_svg2() {
|
499 | let path =
|
500 | BezPath::from_svg("M3.5 8a.5.5 0 01.5-.5h8a.5.5 0 010 1H4a.5.5 0 01-.5-.5z" ).unwrap();
|
501 | assert_eq!(path.segments().count(), 6);
|
502 | }
|
503 |
|
504 | #[test ]
|
505 | fn test_parse_svg_arc() {
|
506 | let path = BezPath::from_svg("M 100 100 A 25 25 0 1 0 -25 25 z" ).unwrap();
|
507 | assert_eq!(path.segments().count(), 3);
|
508 | }
|
509 |
|
510 | // Regression test for #51
|
511 | #[test ]
|
512 | #[allow (clippy::float_cmp)]
|
513 | fn test_parse_svg_arc_pie() {
|
514 | let path = BezPath::from_svg("M 100 100 h 25 a 25 25 0 1 0 -25 25 z" ).unwrap();
|
515 | // Approximate figures, but useful for regression testing
|
516 | assert_eq!(path.area().round(), -1473.0);
|
517 | assert_eq!(path.perimeter(1e-6).round(), 168.0);
|
518 | }
|
519 |
|
520 | #[test ]
|
521 | fn test_write_svg_single() {
|
522 | let segments = [CubicBez::new(
|
523 | Point::new(10., 10.),
|
524 | Point::new(20., 20.),
|
525 | Point::new(30., 30.),
|
526 | Point::new(40., 40.),
|
527 | )
|
528 | .into()];
|
529 | let path = BezPath::from_path_segments(segments.iter().cloned());
|
530 |
|
531 | assert_eq!(path.to_svg(), "M10,10 C20,20 30,30 40,40" );
|
532 | }
|
533 |
|
534 | #[test ]
|
535 | fn test_write_svg_two_nomove() {
|
536 | let segments = [
|
537 | CubicBez::new(
|
538 | Point::new(10., 10.),
|
539 | Point::new(20., 20.),
|
540 | Point::new(30., 30.),
|
541 | Point::new(40., 40.),
|
542 | )
|
543 | .into(),
|
544 | CubicBez::new(
|
545 | Point::new(40., 40.),
|
546 | Point::new(30., 30.),
|
547 | Point::new(20., 20.),
|
548 | Point::new(10., 10.),
|
549 | )
|
550 | .into(),
|
551 | ];
|
552 | let path = BezPath::from_path_segments(segments.iter().cloned());
|
553 |
|
554 | assert_eq!(
|
555 | path.to_svg(),
|
556 | "M10,10 C20,20 30,30 40,40 C30,30 20,20 10,10"
|
557 | );
|
558 | }
|
559 |
|
560 | #[test ]
|
561 | fn test_write_svg_two_move() {
|
562 | let segments = [
|
563 | CubicBez::new(
|
564 | Point::new(10., 10.),
|
565 | Point::new(20., 20.),
|
566 | Point::new(30., 30.),
|
567 | Point::new(40., 40.),
|
568 | )
|
569 | .into(),
|
570 | CubicBez::new(
|
571 | Point::new(50., 50.),
|
572 | Point::new(30., 30.),
|
573 | Point::new(20., 20.),
|
574 | Point::new(10., 10.),
|
575 | )
|
576 | .into(),
|
577 | ];
|
578 | let path = BezPath::from_path_segments(segments.iter().cloned());
|
579 |
|
580 | assert_eq!(
|
581 | path.to_svg(),
|
582 | "M10,10 C20,20 30,30 40,40 M50,50 C30,30 20,20 10,10"
|
583 | );
|
584 | }
|
585 |
|
586 | use rand::prelude::*;
|
587 |
|
588 | fn gen_random_path_sequence(rng: &mut impl Rng) -> Vec<PathSeg> {
|
589 | const MAX_LENGTH: u32 = 10;
|
590 |
|
591 | let mut elements = vec![];
|
592 | let mut position = None;
|
593 |
|
594 | let length = rng.gen_range(0..MAX_LENGTH);
|
595 | for _ in 0..length {
|
596 | let should_follow: bool = rand::random();
|
597 | let kind = rng.gen_range(0..3);
|
598 |
|
599 | let first = position
|
600 | .filter(|_| should_follow)
|
601 | .unwrap_or_else(|| Point::new(rng.gen(), rng.gen()));
|
602 |
|
603 | let element: PathSeg = match kind {
|
604 | 0 => Line::new(first, Point::new(rng.gen(), rng.gen())).into(),
|
605 |
|
606 | 1 => QuadBez::new(
|
607 | first,
|
608 | Point::new(rng.gen(), rng.gen()),
|
609 | Point::new(rng.gen(), rng.gen()),
|
610 | )
|
611 | .into(),
|
612 |
|
613 | 2 => CubicBez::new(
|
614 | first,
|
615 | Point::new(rng.gen(), rng.gen()),
|
616 | Point::new(rng.gen(), rng.gen()),
|
617 | Point::new(rng.gen(), rng.gen()),
|
618 | )
|
619 | .into(),
|
620 |
|
621 | _ => unreachable!(),
|
622 | };
|
623 |
|
624 | position = Some(element.end());
|
625 | elements.push(element);
|
626 | }
|
627 |
|
628 | elements
|
629 | }
|
630 |
|
631 | #[test ]
|
632 | fn test_serialize_deserialize() {
|
633 | const N_TESTS: u32 = 100;
|
634 | let mut rng = rand::thread_rng();
|
635 |
|
636 | for _ in 0..N_TESTS {
|
637 | let vec = gen_random_path_sequence(&mut rng);
|
638 | let ser = BezPath::from_path_segments(vec.iter().cloned()).to_svg();
|
639 | let deser = BezPath::from_svg(&ser).expect("failed deserialization" );
|
640 |
|
641 | let deser_vec = deser.segments().collect::<Vec<PathSeg>>();
|
642 |
|
643 | assert_eq!(vec, deser_vec);
|
644 | }
|
645 | }
|
646 | }
|
647 | |