use svgtypes::{PathParser, PathSegment};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathSeg {
MoveTo {
x: f32,
y: f32,
},
LineTo {
x: f32,
y: f32,
},
QuadTo {
c1x: f32,
c1y: f32,
x: f32,
y: f32,
},
CubicTo {
c1x: f32,
c1y: f32,
c2x: f32,
c2y: f32,
x: f32,
y: f32,
},
Close,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PathData(pub Vec<PathSeg>);
impl PathData {
pub(crate) fn parse(d: &str) -> Result<PathData, String> {
let mut segs = Vec::new();
let (mut cx, mut cy) = (0.0f64, 0.0f64); let (mut sx, mut sy) = (0.0f64, 0.0f64); let mut prev_cubic: Option<(f64, f64)> = None;
let mut prev_quad: Option<(f64, f64)> = None;
for seg in PathParser::from(d) {
let seg = seg.map_err(|e| format!("invalid path data {d:?}: {e}"))?;
let abs = |is_abs: bool, x: f64, y: f64| {
if is_abs { (x, y) } else { (cx + x, cy + y) }
};
match seg {
PathSegment::MoveTo { abs: a, x, y } => {
(cx, cy) = abs(a, x, y);
(sx, sy) = (cx, cy);
(prev_cubic, prev_quad) = (None, None);
segs.push(PathSeg::MoveTo {
x: cx as f32,
y: cy as f32,
});
}
PathSegment::LineTo { abs: a, x, y } => {
(cx, cy) = abs(a, x, y);
(prev_cubic, prev_quad) = (None, None);
segs.push(PathSeg::LineTo {
x: cx as f32,
y: cy as f32,
});
}
PathSegment::HorizontalLineTo { abs: a, x } => {
cx = if a { x } else { cx + x };
(prev_cubic, prev_quad) = (None, None);
segs.push(PathSeg::LineTo {
x: cx as f32,
y: cy as f32,
});
}
PathSegment::VerticalLineTo { abs: a, y } => {
cy = if a { y } else { cy + y };
(prev_cubic, prev_quad) = (None, None);
segs.push(PathSeg::LineTo {
x: cx as f32,
y: cy as f32,
});
}
PathSegment::CurveTo {
abs: a,
x1,
y1,
x2,
y2,
x,
y,
} => {
let (c1x, c1y) = abs(a, x1, y1);
let (c2x, c2y) = abs(a, x2, y2);
(cx, cy) = abs(a, x, y);
(prev_cubic, prev_quad) = (Some((c2x, c2y)), None);
segs.push(PathSeg::CubicTo {
c1x: c1x as f32,
c1y: c1y as f32,
c2x: c2x as f32,
c2y: c2y as f32,
x: cx as f32,
y: cy as f32,
});
}
PathSegment::SmoothCurveTo {
abs: a,
x2,
y2,
x,
y,
} => {
let (px, py) = prev_cubic.unwrap_or((cx, cy));
let (c1x, c1y) = (2.0 * cx - px, 2.0 * cy - py);
let (c2x, c2y) = abs(a, x2, y2);
(cx, cy) = abs(a, x, y);
(prev_cubic, prev_quad) = (Some((c2x, c2y)), None);
segs.push(PathSeg::CubicTo {
c1x: c1x as f32,
c1y: c1y as f32,
c2x: c2x as f32,
c2y: c2y as f32,
x: cx as f32,
y: cy as f32,
});
}
PathSegment::Quadratic {
abs: a,
x1,
y1,
x,
y,
} => {
let (qx, qy) = abs(a, x1, y1);
(cx, cy) = abs(a, x, y);
(prev_cubic, prev_quad) = (None, Some((qx, qy)));
segs.push(PathSeg::QuadTo {
c1x: qx as f32,
c1y: qy as f32,
x: cx as f32,
y: cy as f32,
});
}
PathSegment::SmoothQuadratic { abs: a, x, y } => {
let (px, py) = prev_quad.unwrap_or((cx, cy));
let (qx, qy) = (2.0 * cx - px, 2.0 * cy - py);
(cx, cy) = abs(a, x, y);
(prev_cubic, prev_quad) = (None, Some((qx, qy)));
segs.push(PathSeg::QuadTo {
c1x: qx as f32,
c1y: qy as f32,
x: cx as f32,
y: cy as f32,
});
}
PathSegment::EllipticalArc { .. } => {
return Err(format!("arc segments unsupported in v1 in path data {d:?}"));
}
PathSegment::ClosePath { .. } => {
(cx, cy) = (sx, sy);
(prev_cubic, prev_quad) = (None, None);
segs.push(PathSeg::Close);
}
}
}
Ok(PathData(segs))
}
}
#[cfg(test)]
mod tests {
use super::{PathData, PathSeg};
#[test]
fn mixed_relative_absolute_normalizes() {
let d = PathData::parse("M10 10 l10 0 q5 5 10 0 c1 2 3 4 5 6 z").expect("valid path");
assert_eq!(
d.0,
vec![
PathSeg::MoveTo { x: 10.0, y: 10.0 },
PathSeg::LineTo { x: 20.0, y: 10.0 },
PathSeg::QuadTo {
c1x: 25.0,
c1y: 15.0,
x: 30.0,
y: 10.0
},
PathSeg::CubicTo {
c1x: 31.0,
c1y: 12.0,
c2x: 33.0,
c2y: 14.0,
x: 35.0,
y: 16.0
},
PathSeg::Close,
]
);
}
#[test]
fn h_v_and_close_normalize() {
let d = PathData::parse("M1 2 H5 v3 h-2 Z l1 1").expect("valid path");
assert_eq!(
d.0,
vec![
PathSeg::MoveTo { x: 1.0, y: 2.0 },
PathSeg::LineTo { x: 5.0, y: 2.0 },
PathSeg::LineTo { x: 5.0, y: 5.0 },
PathSeg::LineTo { x: 3.0, y: 5.0 },
PathSeg::Close,
PathSeg::LineTo { x: 2.0, y: 3.0 },
]
);
}
#[test]
fn smooth_shorthands_expand_via_reflection() {
let d = PathData::parse("M0 0 C1 1 2 1 3 0 S5 -1 6 0").expect("valid path");
assert_eq!(
d.0[2],
PathSeg::CubicTo {
c1x: 4.0,
c1y: -1.0,
c2x: 5.0,
c2y: -1.0,
x: 6.0,
y: 0.0
}
);
let d = PathData::parse("M0 0 Q1 2 2 0 T4 0").expect("valid path");
assert_eq!(
d.0[2],
PathSeg::QuadTo {
c1x: 3.0,
c1y: -2.0,
x: 4.0,
y: 0.0
}
);
let d = PathData::parse("M5 5 T9 9").expect("valid path");
assert_eq!(
d.0[1],
PathSeg::QuadTo {
c1x: 5.0,
c1y: 5.0,
x: 9.0,
y: 9.0
}
);
}
#[test]
fn garbage_input_errors() {
assert!(PathData::parse("M10 10 L nope").is_err());
assert!(PathData::parse("L10 10").is_err());
}
#[test]
fn arcs_are_rejected_whole() {
let err = PathData::parse("M0 0 A5 5 0 0 1 10 10").expect_err("arc must be rejected");
assert!(err.contains("arc segments unsupported"), "{err}");
}
#[test]
fn empty_input_is_an_empty_path() {
assert_eq!(PathData::parse("").expect("valid"), PathData::default());
}
}