From 7a1585e39e95fbdfd403ca3501ae4d90a872b9e5 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 9 Apr 2023 12:30:22 +0100 Subject: [PATCH 1/5] Lay groundwork for svg path --- .gitignore | 1 + src/lib.rs | 1 + src/paths/mod.rs | 1 + src/paths/svg.rs | 29 +++++++++++++++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 src/paths/mod.rs create mode 100644 src/paths/svg.rs diff --git a/.gitignore b/.gitignore index 69369904..173b9514 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk Cargo.lock +.vscode diff --git a/src/lib.rs b/src/lib.rs index 203e367b..50300f9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ mod line; mod mindist; pub mod offset; mod param_curve; +mod paths; mod point; mod quadbez; mod quadspline; diff --git a/src/paths/mod.rs b/src/paths/mod.rs new file mode 100644 index 00000000..bbb1356c --- /dev/null +++ b/src/paths/mod.rs @@ -0,0 +1 @@ +mod svg; diff --git a/src/paths/svg.rs b/src/paths/svg.rs new file mode 100644 index 00000000..48cc4f0f --- /dev/null +++ b/src/paths/svg.rs @@ -0,0 +1,29 @@ +use crate::Point; + +/// An SVG path +pub struct SvgPath(Vec); + +/// An SVG path element +pub enum PathEl { + /// Start a new sub-path at the given (x,y) coordinates. + MoveTo(Point), + /// Close the current subpath by connecting it back to the current subpath's initial point. + ClosePath, + /// Draw a line from the current point to the given point. + LineTo(Point), + /// Draw a cubic Bezier curve from the current point to `to`. + /// + /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest + /// to `to`. + CurveTo { + to: Point, + ctrl1: Point, + ctrl2: Point, + }, +} + +struct CurveTo { + to: Point, + ctrl1: Point, + ctrl2: Point, +} From b4b33b7ea3625aca57e563d9337b93aa4461e9ef Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 9 Apr 2023 20:41:13 +0100 Subject: [PATCH 2/5] WIP --- Cargo.toml | 9 +- src/bezpath.rs | 25 +++ src/lib.rs | 2 +- src/paths/mod.rs | 9 +- src/paths/svg.rs | 524 +++++++++++++++++++++++++++++++++++++++++++++-- src/svg.rs | 47 +---- 6 files changed, 549 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 71203966..c236fe50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ features = ["mint", "schemars", "serde"] [features] default = ["std"] std = [] +serde = ["smallvec/serde", "arrayvec/serde", "serde1"] + +[dependencies] +smallvec = "1.10.0" +anyhow = "1.0.70" [dependencies.arrayvec] version = "0.7.1" @@ -33,8 +38,9 @@ optional = true version = "0.8.6" optional = true -[dependencies.serde] +[dependencies.serde1] version = "1.0.105" +package = "serde" optional = true default-features = false features = ["derive"] @@ -45,4 +51,3 @@ rand = "0.8.0" [target.'cfg(target_arch="wasm32")'.dev-dependencies] getrandom = { version = "0.2.0", features = ["js"] } - diff --git a/src/bezpath.rs b/src/bezpath.rs index 5a5903ec..e1b4baff 100644 --- a/src/bezpath.rs +++ b/src/bezpath.rs @@ -199,6 +199,31 @@ impl BezPath { BezPath(v) } + /// Create a BezPath with segments corresponding to the sequence of `PathSeg`s. + /// + /// This constructor will insert [`PathEl::MoveTo`]s as needed so that drawing the returned + /// `BezPath` is equivalent to drawing each of the `PathSeg`s. + pub fn from_path_segments(segments: impl Iterator) -> BezPath { + let mut path_elements = Vec::new(); + let mut current_pos = None; + + for segment in segments { + let start = segment.start(); + if Some(start) != current_pos { + path_elements.push(PathEl::MoveTo(start)); + }; + path_elements.push(match segment { + PathSeg::Line(l) => PathEl::LineTo(l.p1), + PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2), + PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3), + }); + + current_pos = Some(segment.end()); + } + + BezPath::from_vec(path_elements) + } + /// Removes the last [`PathEl`] from the path and returns it, or `None` if the path is empty. pub fn pop(&mut self) -> Option { self.0.pop() diff --git a/src/lib.rs b/src/lib.rs index 50300f9f..ada1d3e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,7 +98,7 @@ mod line; mod mindist; pub mod offset; mod param_curve; -mod paths; +pub mod paths; mod point; mod quadbez; mod quadspline; diff --git a/src/paths/mod.rs b/src/paths/mod.rs index bbb1356c..89a11cb0 100644 --- a/src/paths/mod.rs +++ b/src/paths/mod.rs @@ -1 +1,8 @@ -mod svg; +//! Different path representations, useful in different contexts. +//! +//! In `kurbo`, the canonical path representation is `BezPath`, which is always drawn with perfect +//! accuracy. However, sometimes it is useful to represent paths in different ways, for example +//! circular arcs are often used in architecture, and SVG defines a different path model to +//! `BezPath`, so they cannot be used interchangeably. + +pub mod svg; diff --git a/src/paths/svg.rs b/src/paths/svg.rs index 48cc4f0f..d1d9317b 100644 --- a/src/paths/svg.rs +++ b/src/paths/svg.rs @@ -1,29 +1,519 @@ -use crate::Point; +//! This module provides type [`Path`] representing SVG path data, and associated types. +use crate::{PathEl as KurboPathEl, Point, Shape, Vec2}; +use anyhow::{anyhow, Result}; +use std::{fmt, ops::Deref, vec}; + +pub use self::one_vec::*; /// An SVG path -pub struct SvgPath(Vec); +/// +/// A path *MUST* begin with a `MoveTo` element. For this and other invalid inputs, we return +/// an error value explaining the problem with the input. +/// +/// Based on [the SVG path data specification](https://svgwg.org/specs/paths/#PathData). +#[derive(Clone, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Path { + /// The elements that make up this path. + elements: Vec, +} /// An SVG path element +/// +/// # Bearing and relative position +/// +/// Relative path elements are affected by the end position of the previous path and the bearing. +/// You can think of the relative elements as being specified in the coordinate space that consists +/// of a translation to the previous point, followed by a rotation by the bearing. The bearing +/// rotation is at *0* along the *x* axis, and then proceeds in the positive *y* direction. In the +/// y-down SVG coordinate system, this correspond to clockwise rotation. +// TODO think about if we can store things on the stack more. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PathEl { - /// Start a new sub-path at the given (x,y) coordinates. - MoveTo(Point), - /// Close the current subpath by connecting it back to the current subpath's initial point. + /// `M`: Start a new sub-path at the given point. + /// + /// Subsequent points indicate that straight lines should be drawn (equivalent to `LineTo`). + /// This can be confusing, and only saves 1 character per polyline, but it is in + /// [the spec](https://svgwg.org/specs/paths/#PathDataMovetoCommands). + MoveTo(OneVec), + /// `m`: Start a new sub-path at the given point, relative to the previous end point. + /// + /// If there is no previous end point (because this is the first element of the path), then the + /// value is interpreted as an absolute location. The points are interpreted taking into account + /// the bearing and previous position, as detailed in the [type documentation][PathEl]. + MoveToRel(OneVec), + /// `z`, `Z`: Close the current subpath by connecting it back to the current subpath's initial + /// point. + /// + /// This is one of the places where this struct is lossy - it will always output `Z` even if the + /// input was `z`. These are both semantically equivalent. ClosePath, - /// Draw a line from the current point to the given point. - LineTo(Point), - /// Draw a cubic Bezier curve from the current point to `to`. + /// `L`: Draw a line from the current point to the given point. + /// + /// Each point is interpreted as the endpoint of another line. + LineTo(OneVec), + /// `l`: Draw a line from the current point to the given point, with the offsets given relative + /// to the current position and bearing. + /// + /// Each offset is interpreted as another line. The points are interpreted taking into account + /// the bearing and previous position, as detailed in the [type documentation][PathEl]. + LineToRel(OneVec), + /// `H`: Draw a horizontal line with the given distance. + /// + /// Multiple values are interpreted as multiple horizontal lines. + Horiz(OneVec), + /// `h`: Draw a line with the given distance along the bearing direction. + /// + /// Multiple values are interpreted as multiple lines. The points are interpreted taking into + /// account the bearing and previous position, as detailed in the [type documentation][PathEl]. + HorizRel(OneVec), + /// `V`: Draw a vertical line with the given distance. + /// + /// Multiple values are interpreted as multiple lines. + Vert(OneVec), + /// `v`: Draw a line with the given distance at right angles to the bearing direction. + /// + /// Multiple values are interpreted as multiple lines. The points are interpreted taking into + /// account the bearing and previous position, as detailed in the [type documentation][PathEl]. + VertRel(OneVec), + /// `C`: Draw a cubic Bezier curve from the current point to `to`. + /// + /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest + /// to `to`. + CubicTo(OneVec), + /// `c`: Draw a cubic Bezier curve from the current point to `to`. + /// + /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest + /// to `to`. The points are interpreted taking into account the bearing and previous position, + /// as detailed in the [type documentation][PathEl]. + CubicToRel(OneVec), + /// `S`: Draw a smooth cubic Bezier curve from the current point to `to`. + /// + /// `ctrl2` is the control point nearest to `to`. The first control point is calculated from the + /// previous input (see [`SmoothCubicTo`]). + SmoothCubicTo(OneVec), + /// `s`: Draw a cubic Bezier curve from the current point to `to`. + /// + /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest + /// to `to`. The first control point is calculated from the previous input (see + /// [`SmoothCubicTo`]). Both the actual and inferred points are interpreted taking into account + /// the bearing and previous position, as detailed in the [type documentation][PathEl]. + SmoothCubicToRel(OneVec), + /// `Q`: Draw a cubic Bezier curve from the current point to `to`. /// /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest /// to `to`. - CurveTo { - to: Point, - ctrl1: Point, - ctrl2: Point, - }, + QuadTo(OneVec), + /// `q`: Draw a cubic Bezier curve from the current point to `to`. + /// + /// `ctrl1` is the control point nearest to the start, and `ctrl2` is the control point nearest + /// to `to`. The points are interpreted taking into account the bearing and previous position, + /// as detailed in the [type documentation][PathEl]. + QuadToRel(OneVec), + /// `T`: Draw a smooth cubic Bezier curve from the current point to `to`. + /// + /// The control point is calculated from the previous input, either a reflection of the previous + /// control point, or the start point if this is the first segment, or the previous segment was + /// not a quadratic Bezier. + SmoothQuadTo(OneVec), + /// `t`: Draw a cubic Bezier curve from the current point to `to`. + /// + /// The control point is calculated from the previous input, either a reflection of the previous + /// control point, or the start point if this is the first segment, or the previous segment was + /// not a quadratic Bezier. Both the actual and inferred points are interpreted taking into + /// account the bearing and previous position, as detailed in the [type documentation][PathEl]. + SmoothQuadToRel(OneVec), + /// `A`: Draw an elliptical arc. + /// + /// See the documentation for [`Arc`] for more details. + EllipticArc(OneVec), + /// `a`: Draw an elliptical arc. + /// + /// See the documentation for [`Arc`] for more details. The points are interpreted taking into + /// account the bearing and previous position, as detailed in the [type documentation][PathEl]. + /// In particular, this affects the x-axis rotation (which will be the sum of the given x-axis + /// rotation and the bearing). + EllipticArcRel(OneVec), + // TODO catmull-rom. These curves look interesting but I don't know anything about them and + // so it makes sense to tackle them later. + /// Set the bearing. + /// + /// This overwrites the existing bearing. This is another place where this implementation is + /// lossy: the specification allows for multiple bearing commands in sequence, but if we + /// encounter this we collapse them down to a single bearing. In the absolute case this means + /// taking the last bearing and discarding the others. + Bearing(f64), + /// Set the bearing. + /// + /// This differs from `Bearing` in that it adds the parameter to the current bearing, rather + /// than overwriting it. This is another place where this implementation is lossy: the + /// specification allows for multiple bearing commands in sequence, but if we encounter this we + /// collapse them down to a single bearing. In the relative case this means summing the + /// bearings (mod 2pi). + BearingRel(f64), +} + +/// The parameters of a `CubicTo` or `CubicToRel` element. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CubicTo { + /// The point that this curve ends at + pub to: Point, + /// The first control point (from the start) + pub ctrl1: Point, + /// The second control point (from the start) + pub ctrl2: Point, +} + +/// The parameters of a `SmoothCubicTo` or `CubicToRel` element. +/// +/// The first control point is the reflection of the control point from the previous curve, or the +/// start point if there was no previous point, or the previous point was not a cubic Bezier. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SmoothCubicTo { + /// The point that this curve ends at + pub to: Point, + /// The second control point (from the start) + pub ctrl2: Point, +} + +/// The parameters of a `QuadTo` or `QuadToRel` element. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct QuadTo { + /// The point that this curve ends at + pub to: Point, + /// The control point + pub ctrl: Point, +} + +// Note: the SVG arc logic is heavily adapted from https://github.com/nical/lyon +/// An SVG arc segment. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Arc { + /// The arc's end point. + pub to: Point, + /// The arc's radii, where the vector's x-component is the radius in the + /// positive x direction after applying `x_rotation`. + pub radii: Vec2, + /// How much the arc is rotated, in radians. + pub x_rotation: f64, + /// Does this arc sweep through more than π radians? + pub large_arc: bool, + /// Determines if the arc should begin moving at positive angles. + pub sweep: bool, +} + +impl Path { + /// Create a new path object. + pub fn new() -> Self { + Self { elements: vec![] } + } + + /// Push an element onto the end of an array. + /// + /// All elements apart from `MoveTo` and `Bearing` must be preceeded by a `MoveTo`. + pub fn push(&mut self, el: PathEl) -> Result<()> { + // bearings and moveto are always allowed + if let PathEl::MoveTo(_) = &el { + self.elements.push(el); + return Ok(()); + } + if let PathEl::Bearing(_) = &el { + self.elements.push(el); + return Ok(()); + } + + // other elements are only allowed if they come after a moveto + if self + .elements + .iter() + .find(|el| matches!(*el, PathEl::MoveTo(_))) + .is_none() + { + return Err(anyhow!( + "all line and curve elements must be preceeded by a moveto" + )); + } + + self.elements.push(el); + Ok(()) + } + + /// Write out a text representation of the string. + pub fn write(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut iter = self.elements.iter(); + if let Some(el) = iter.next() { + el.write(f)?; + } + for el in iter { + write!(f, " ")?; + el.write(f)?; + } + Ok(()) + } +} + +impl Deref for Path { + type Target = [PathEl]; + + fn deref(&self) -> &[PathEl] { + &self.elements + } +} + +impl Shape for Path { + type PathElementsIter<'iter> = PathElementsIter<'iter>; + + fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> { + PathElementsIter { + path: self, + tolerance, + path_start_point: Point::ZERO, + current_point: Point::ZERO, + current_bearing: 0., + } + } + + fn area(&self) -> f64 { + todo!() + } + + fn perimeter(&self, accuracy: f64) -> f64 { + self.elements.iter().map(PathEl::length).sum() + } + + fn winding(&self, pt: Point) -> i32 { + todo!() + } + + fn bounding_box(&self) -> crate::Rect { + todo!() + } +} + +impl PathEl { + fn length(&self) -> f64 { + todo!() + } + + fn write(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PathEl::MoveTo(points) => { + write!(f, "M")?; + points.write_spaced(write_point, f)?; + } + PathEl::MoveToRel(points) => { + write!(f, "m")?; + points.write_spaced(write_point, f)?; + } + PathEl::ClosePath => { + write!(f, "Z")?; + } + PathEl::LineTo(points) => { + write!(f, "L")?; + points.write_spaced(write_point, f)?; + } + PathEl::LineToRel(points) => { + write!(f, "l")?; + points.write_spaced(write_point, f)?; + } + PathEl::Horiz(amts) => { + write!(f, "H")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::HorizRel(amts) => { + write!(f, "h")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::Vert(amts) => { + write!(f, "V")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::VertRel(amts) => { + write!(f, "v")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::CubicTo(cubic_tos) => { + write!(f, "C")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::CubicToRel(cubic_tos) => { + write!(f, "c")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::SmoothCubicTo(cubic_tos) => { + write!(f, "S")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::SmoothCubicToRel(cubic_tos) => { + write!(f, "s")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::QuadTo(quad_tos) => { + write!(f, "Q")?; + quad_tos.write_spaced(QuadTo::write_vals, f)?; + } + PathEl::QuadToRel(quad_tos) => { + write!(f, "q")?; + quad_tos.write_spaced(QuadTo::write_vals, f)?; + } + PathEl::SmoothQuadTo(quad_tos) => { + write!(f, "T")?; + quad_tos.write_spaced(write_point, f)?; + } + PathEl::SmoothQuadToRel(quad_tos) => { + write!(f, "t")?; + quad_tos.write_spaced(write_point, f)?; + } + PathEl::EllipticArc(_) => todo!(), + PathEl::EllipticArcRel(_) => todo!(), + PathEl::Bearing(bearing) => { + write!(f, "B{bearing}",)?; + } + PathEl::BearingRel(bearing) => { + write!(f, "b{bearing}",)?; + } + } + Ok(()) + } +} + +impl CubicTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{} {},{}", + self.ctrl1.x, self.ctrl1.y, self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y + ) + } +} + +impl SmoothCubicTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{}", + self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y + ) + } } -struct CurveTo { - to: Point, - ctrl1: Point, - ctrl2: Point, +impl QuadTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{}", + self.ctrl.x, self.ctrl.y, self.to.x, self.to.y + ) + } +} + +/// An iterator over the path elements of an SVG path. +/// +/// This structure could be `Copy`, but we don't implement it to avoid hidden clones. +#[derive(Clone)] +pub struct PathElementsIter<'iter> { + /// The path we are traversing. + path: &'iter [PathEl], + /// Tolerance parameter + tolerance: f64, + /// The start point of the current sub-path (this resets for every `MoveTo`). + path_start_point: Point, + /// The end point of the previous segment. + current_point: Point, + /// The current bearing. + current_bearing: f64, +} + +impl<'iter> Iterator for PathElementsIter<'iter> { + type Item = KurboPathEl; + + fn next(&mut self) -> Option { + todo!() + } +} + +fn write_point(Point { x, y }: &Point, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{x},{y}") +} + +mod one_vec { + use anyhow::anyhow; + use std::{fmt, iter, slice, vec}; + + /// A vector that has at least 1 element. + /// + /// It stores the first element on the stack, and the rest on the heap. + #[derive(Debug, Clone)] + pub struct OneVec { + pub first: T, + pub rest: Vec, + } + + impl OneVec { + pub fn single(val: T) -> Self { + Self { + first: val, + rest: vec![], + } + } + + pub fn iter(&self) -> iter::Chain, slice::Iter<'_, T>> { + self.into_iter() + } + + /// Write out the vector with spaces between each element + pub(crate) fn write_spaced( + &self, + mut cb: impl FnMut(&T, &mut fmt::Formatter) -> fmt::Result, + f: &mut fmt::Formatter, + ) -> fmt::Result { + cb(&self.first, f)?; + for v in &self.rest { + cb(v, f)?; + } + Ok(()) + } + } + + impl TryFrom> for OneVec { + type Error = anyhow::Error; + fn try_from(mut v: Vec) -> Result { + // Annoyingly the `Vec::remove` method can panic, so we have to check + // the vec is non-empty + if v.is_empty() { + return Err(anyhow!("vector must not be empty")); + } + let first = v.remove(0); + Ok(OneVec { first, rest: v }) + } + } + + impl<'a, T> IntoIterator for &'a OneVec { + type IntoIter = iter::Chain, slice::Iter<'a, T>>; + type Item = &'a T; + + fn into_iter(self) -> Self::IntoIter { + iter::once(&self.first).chain(&self.rest) + } + } + + impl IntoIterator for OneVec { + type IntoIter = iter::Chain, vec::IntoIter>; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + iter::once(self.first).chain(self.rest) + } + } } diff --git a/src/svg.rs b/src/svg.rs index 67c036fb..c2fc6b2e 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -8,54 +8,9 @@ use std::f64::consts::PI; use std::fmt::{self, Display, Formatter}; use std::io::{self, Write}; -use crate::{Arc, BezPath, ParamCurve, PathEl, PathSeg, Point, Vec2}; - -// Note: the SVG arc logic is heavily adapted from https://github.com/nical/lyon - -/// A single SVG arc segment. -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct SvgArc { - /// The arc's start point. - pub from: Point, - /// The arc's end point. - pub to: Point, - /// The arc's radii, where the vector's x-component is the radius in the - /// positive x direction after applying `x_rotation`. - pub radii: Vec2, - /// How much the arc is rotated, in radians. - pub x_rotation: f64, - /// Does this arc sweep through more than π radians? - pub large_arc: bool, - /// Determines if the arc should begin moving at positive angles. - pub sweep: bool, -} +use crate::{paths::svg::Arc as SvgArc, Arc, BezPath, PathEl, Point, Vec2}; impl BezPath { - /// Create a BezPath with segments corresponding to the sequence of - /// `PathSeg`s - pub fn from_path_segments(segments: impl Iterator) -> BezPath { - let mut path_elements = Vec::new(); - let mut current_pos = None; - - for segment in segments { - let start = segment.start(); - if Some(start) != current_pos { - path_elements.push(PathEl::MoveTo(start)); - }; - path_elements.push(match segment { - PathSeg::Line(l) => PathEl::LineTo(l.p1), - PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2), - PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3), - }); - - current_pos = Some(segment.end()); - } - - BezPath::from_vec(path_elements) - } - /// Convert the path to an SVG path string representation. /// /// The current implementation doesn't take any special care to produce a From 071ccbd07435bc56bf1b759f5c967220a0cdb88b Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 9 Apr 2023 22:21:12 +0100 Subject: [PATCH 3/5] WIP --- Cargo.toml | 4 + examples/svg_path.rs | 69 +++++ src/lib.rs | 8 +- src/paths/svg.rs | 603 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 672 insertions(+), 12 deletions(-) create mode 100644 examples/svg_path.rs diff --git a/Cargo.toml b/Cargo.toml index c236fe50..5b9f6d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,10 @@ features = ["derive"] # This is used for research but not really needed; maybe refactor. [dev-dependencies] rand = "0.8.0" +snog = { git = "https://github.com/derekdreery/snog" } [target.'cfg(target_arch="wasm32")'.dev-dependencies] getrandom = { version = "0.2.0", features = ["js"] } + +[patch.crates-io] +kurbo = { path = "." } diff --git a/examples/svg_path.rs b/examples/svg_path.rs new file mode 100644 index 00000000..b0c3d030 --- /dev/null +++ b/examples/svg_path.rs @@ -0,0 +1,69 @@ +use kurbo::{ + paths::svg::{self, OneVec}, + Affine, Line, Point, Size, +}; +use snog::{ + peniko::{Color, Stroke}, + App, RenderCtx, +}; + +fn main() { + let data = Data::new(); + App::new_with_data(data).with_render(render).run() +} + +// TODO both the lifetime of the RenderCtx and the ref to user data could be the same - nothing is +// gained by having one longer than the other. +fn render(data: &mut Data, mut ctx: RenderCtx<'_>) { + let Size { width, height } = ctx.screen().size(); + + let stroke = Stroke::new(0.005); + let scale = Affine::scale_non_uniform(width, height); + let brush = Color::WHITE; + ctx.stroke(&stroke, scale, &brush, None, &data.path); +} + +struct Data { + path: svg::Path, +} + +impl Data { + fn new() -> Self { + let path = svg::Path::try_from(vec![ + svg::PathEl::MoveTo(OneVec::single(Point::new(0.1, 0.1))), + svg::PathEl::Bearing(std::f64::consts::FRAC_PI_2), + svg::PathEl::LineToRel( + OneVec::try_from(vec![Point::new(0.2, 0.0), Point::new(-0.1, -0.2)]).unwrap(), + ), + svg::PathEl::VertRel(OneVec::single(-0.2)), + svg::PathEl::CubicToRel(OneVec::single(svg::CubicTo { + to: Point::new(0.2, 0.0), + ctrl1: Point::new(0.1, 0.1), + ctrl2: Point::new(0.1, -0.1), + })), + svg::PathEl::SmoothCubicToRel( + OneVec::try_from(vec![ + svg::SmoothCubicTo { + to: Point::new(0.2, 0.0), + ctrl2: Point::new(0.1, -0.1), + }, + svg::SmoothCubicTo { + to: Point::new(0.2, 0.0), + ctrl2: Point::new(0.1, -0.1), + }, + ]) + .unwrap(), + ), + svg::PathEl::QuadToRel(OneVec::single(svg::QuadTo { + to: Point::new(0.0, 0.1), + ctrl: Point::new(0.1, 0.1), + })), + svg::PathEl::SmoothQuadToRel( + OneVec::try_from(vec![Point::new(0.0, 0.1), Point::new(0.0, 0.1)]).unwrap(), + ), + ]) + .unwrap(); + + Data { path } + } +} diff --git a/src/lib.rs b/src/lib.rs index ada1d3e7..bdfcfeb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,8 +108,8 @@ mod rounded_rect_radii; mod shape; pub mod simplify; mod size; -#[cfg(feature = "std")] -mod svg; +//#[cfg(feature = "std")] +//mod svg; mod translate_scale; mod vec2; @@ -131,7 +131,7 @@ pub use crate::rounded_rect::*; pub use crate::rounded_rect_radii::*; pub use crate::shape::*; pub use crate::size::*; -#[cfg(feature = "std")] -pub use crate::svg::*; +//#[cfg(feature = "std")] +//pub use crate::svg::*; pub use crate::translate_scale::*; pub use crate::vec2::*; diff --git a/src/paths/svg.rs b/src/paths/svg.rs index d1d9317b..31044423 100644 --- a/src/paths/svg.rs +++ b/src/paths/svg.rs @@ -1,10 +1,12 @@ //! This module provides type [`Path`] representing SVG path data, and associated types. -use crate::{PathEl as KurboPathEl, Point, Shape, Vec2}; +use crate::{Affine, PathEl as KurboPathEl, Point, Shape, Vec2}; use anyhow::{anyhow, Result}; -use std::{fmt, ops::Deref, vec}; +use std::{f64::consts::PI, fmt, io, iter, mem, ops::Deref, slice, vec}; pub use self::one_vec::*; +type OneIter<'a, T> = iter::Chain, slice::Iter<'a, T>>; + /// An SVG path /// /// A path *MUST* begin with a `MoveTo` element. For this and other invalid inputs, we return @@ -250,7 +252,7 @@ impl Path { } /// Write out a text representation of the string. - pub fn write(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut iter = self.elements.iter(); if let Some(el) = iter.next() { el.write(f)?; @@ -261,6 +263,49 @@ impl Path { } Ok(()) } + + /// Write out the text representation of this path to anything implementing `io::Write`. + /// + /// `Path` also implements [`Display`][std::fmt::Display], which can be used when you need an + /// in-memory string representation (so you can e.g. `path.to_string()`). + /// + /// Note that this call will produce a lot of write calls under the hood, so it is recommended + /// to use a buffer (e.g. [`BufWriter`][std::io::BufWriter]) if your writer's + /// [`write`][io::Write::write] calls are expensive. + pub fn write_to(&self, mut w: impl io::Write) -> io::Result<()> { + write!(w, "{}", self) + } + + /// Returns an error if the path is invalid + fn validate(&self) -> Result<()> { + let move_idx = self + .elements + .iter() + .enumerate() + .find(|(_, el)| matches!(el, PathEl::MoveTo(_) | PathEl::MoveToRel(_))) + .map(|(idx, _)| idx); + let path_idx = self + .elements + .iter() + .enumerate() + .find(|(_, el)| { + !matches!( + el, + PathEl::MoveTo(_) + | PathEl::MoveToRel(_) + | PathEl::Bearing(_) + | PathEl::BearingRel(_) + ) + }) + .map(|(idx, _)| idx); + match (move_idx, path_idx) { + (None, Some(idx)) => Err(anyhow!("First path at index {idx} before first move")), + (Some(move_idx), Some(path_idx)) if move_idx > path_idx => Err(anyhow!( + "First path at index {path_idx} before first move at index {move_idx}" + )), + _ => Ok(()), + } + } } impl Deref for Path { @@ -271,6 +316,22 @@ impl Deref for Path { } } +impl TryFrom> for Path { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> Result { + let path = Path { elements: value }; + path.validate()?; + Ok(path) + } +} + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt(f) + } +} + impl Shape for Path { type PathElementsIter<'iter> = PathElementsIter<'iter>; @@ -281,6 +342,10 @@ impl Shape for Path { path_start_point: Point::ZERO, current_point: Point::ZERO, current_bearing: 0., + state: IterState::None, + previous_cubic: None, + previous_quad: None, + seen_moveto: false, } } @@ -353,11 +418,11 @@ impl PathEl { } PathEl::SmoothCubicTo(cubic_tos) => { write!(f, "S")?; - cubic_tos.write_spaced(CubicTo::write_vals, f)?; + cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; } PathEl::SmoothCubicToRel(cubic_tos) => { write!(f, "s")?; - cubic_tos.write_spaced(CubicTo::write_vals, f)?; + cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; } PathEl::QuadTo(quad_tos) => { write!(f, "Q")?; @@ -375,8 +440,14 @@ impl PathEl { write!(f, "t")?; quad_tos.write_spaced(write_point, f)?; } - PathEl::EllipticArc(_) => todo!(), - PathEl::EllipticArcRel(_) => todo!(), + PathEl::EllipticArc(arcs) => { + write!(f, "A")?; + arcs.write_spaced(Arc::write_vals, f)?; + } + PathEl::EllipticArcRel(arcs) => { + write!(f, "a")?; + arcs.write_spaced(Arc::write_vals, f)?; + } PathEl::Bearing(bearing) => { write!(f, "B{bearing}",)?; } @@ -418,6 +489,22 @@ impl QuadTo { } } +impl Arc { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {} {},{} {},{}", + self.radii.x, + self.radii.y, + self.x_rotation, + self.large_arc, + self.sweep, + self.to.x, + self.to.y + ) + } +} + /// An iterator over the path elements of an SVG path. /// /// This structure could be `Copy`, but we don't implement it to avoid hidden clones. @@ -433,13 +520,467 @@ pub struct PathElementsIter<'iter> { current_point: Point, /// The current bearing. current_bearing: f64, + /// This flag tracks whether we have seen a moveto command yet. It affects the behavior of + /// `MoveToRel`. + seen_moveto: bool, + /// If there was a previous cubic bezier value, store its ctrl2 here. + /// + /// The point is ctrl2 in absolute coordinates (since this is all that's needed for smooth + /// cubics). + previous_cubic: Option, + /// If there was a previous quad bezier value, store its ctrl here. + /// + /// Format is ctrl in absolute coordinates. + previous_quad: Option, + /// Iterator state machine + state: IterState<'iter>, +} + +#[derive(Clone)] +enum IterState<'iter> { + /// We aren't part-way through any data type. + None, + /// We're in the middle of a lineto or moveto. + /// + /// These values are actually for drawing lines to. See the spec for details of why this is the + /// state for a `MoveTo` as well. + LineTo { + transform: bool, + rest: &'iter [Point], + }, + /// Horizontal lines with the given distance + HorizTo { transform: bool, rest: &'iter [f64] }, + /// Vertical lines with the given distance + VertTo { transform: bool, rest: &'iter [f64] }, + /// Cubic Beziers + CubicTo { + transform: bool, + rest: &'iter [CubicTo], + }, + /// Smooth cubic Beziers + SmoothCubicTo { + transform: bool, + rest: &'iter [SmoothCubicTo], + }, + /// Quad Beziers + QuadTo { + transform: bool, + rest: &'iter [QuadTo], + }, + /// Smooth quad Beziers + SmoothQuadTo { + transform: bool, + rest: &'iter [Point], + }, +} + +impl<'iter> PathElementsIter<'iter> { + /// Handle the next element + /// + /// Only call this if we finished handling the previous one (i.e. `state = IterState::None`). + fn next_el(&mut self, el: &'iter PathEl) -> Option { + match el { + PathEl::MoveTo(points) => { + self.seen_moveto = true; + let (first, rest) = points.split(); + self.current_point = *first; + self.path_start_point = *first; + self.state = IterState::LineTo { + transform: false, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::MoveTo(*first)) + } + PathEl::MoveToRel(points) => { + let (&first, rest) = points.split(); + self.reset_prev_beziers(); + if self.seen_moveto { + let first = self.transform() * first; + self.current_point = first; + self.path_start_point = first; + self.state = IterState::LineTo { + transform: true, + rest, + }; + Some(KurboPathEl::MoveTo(first)) + } else { + self.seen_moveto = true; + self.current_point = first; + self.path_start_point = first; + // Even though we treat the first element as absolute, we still treat + // subsequent points as `LineToRel`s. This might make a difference if the + // user added a `Bearing` before the first `MoveTo`. + self.state = IterState::LineTo { + transform: true, + rest, + }; + Some(KurboPathEl::MoveTo(first)) + } + } + PathEl::ClosePath => { + self.current_point = self.path_start_point; + Some(KurboPathEl::ClosePath) + } + PathEl::LineTo(points) => { + let (&first, rest) = points.split(); + self.current_point = first; + self.state = IterState::LineTo { + transform: false, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::LineToRel(points) => { + let (&first, rest) = points.split(); + let first = self.transform() * first; + self.current_point = first; + self.state = IterState::LineTo { + transform: true, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::Horiz(dists) => { + let (&first, rest) = dists.split(); + let first = Point::new(first, 0.); + self.current_point = first; + self.state = IterState::HorizTo { + transform: false, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::HorizRel(dists) => { + let (&first, rest) = dists.split(); + let first = self.transform() * Point::new(first, 0.); + self.current_point = first; + self.state = IterState::HorizTo { + transform: true, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::Vert(dists) => { + let (&first, rest) = dists.split(); + let first = Point::new(0., first); + self.current_point = first; + self.state = IterState::VertTo { + transform: false, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::VertRel(dists) => { + let (&first, rest) = dists.split(); + let first = self.transform() * Point::new(0., first); + self.current_point = first; + self.state = IterState::VertTo { + transform: true, + rest, + }; + self.reset_prev_beziers(); + Some(KurboPathEl::LineTo(first)) + } + PathEl::CubicTo(cubics) => { + let (CubicTo { to, ctrl1, ctrl2 }, rest) = cubics.split(); + self.current_point = *to; + self.state = IterState::CubicTo { + transform: false, + rest, + }; + self.previous_quad = None; + self.previous_cubic = Some(*ctrl2); + Some(KurboPathEl::CurveTo(*ctrl1, *ctrl2, *to)) + } + PathEl::CubicToRel(cubics) => { + let (CubicTo { to, ctrl1, ctrl2 }, rest) = cubics.split(); + let (to, ctrl1, ctrl2) = { + let trans = self.transform(); + (trans * *to, trans * *ctrl1, trans * *ctrl2) + }; + self.current_point = to; + self.state = IterState::CubicTo { + transform: true, + rest, + }; + self.previous_quad = None; + self.previous_cubic = Some(ctrl2); + Some(KurboPathEl::CurveTo(ctrl1, ctrl2, to)) + } + PathEl::SmoothCubicTo(cubics) => { + let (SmoothCubicTo { to, ctrl2 }, rest) = cubics.split(); + let ctrl1 = self.get_smooth_cubic_ctrl1(); + self.current_point = *to; + self.state = IterState::SmoothCubicTo { + transform: false, + rest, + }; + self.previous_quad = None; + self.previous_cubic = Some(*ctrl2); + Some(KurboPathEl::CurveTo(ctrl1, *ctrl2, *to)) + } + PathEl::SmoothCubicToRel(cubics) => { + let (SmoothCubicTo { to, ctrl2 }, rest) = cubics.split(); + + let (to, ctrl2) = { + let trans = self.transform(); + (trans * *to, trans * *ctrl2) + }; + let ctrl1 = self.get_smooth_cubic_ctrl1(); + self.current_point = to; + self.state = IterState::SmoothCubicTo { + transform: true, + rest, + }; + self.previous_quad = None; + self.previous_cubic = Some(ctrl2); + Some(KurboPathEl::CurveTo(ctrl1, ctrl2, to)) + } + PathEl::QuadTo(cubics) => { + let (QuadTo { to, ctrl }, rest) = cubics.split(); + self.current_point = *to; + self.state = IterState::QuadTo { + transform: false, + rest, + }; + self.previous_quad = Some(*ctrl); + self.previous_cubic = None; + Some(KurboPathEl::QuadTo(*ctrl, *to)) + } + PathEl::QuadToRel(cubics) => { + let (QuadTo { to, ctrl }, rest) = cubics.split(); + let (to, ctrl) = { + let trans = self.transform(); + (trans * *to, trans * *ctrl) + }; + + self.current_point = to; + self.state = IterState::QuadTo { + transform: true, + rest, + }; + self.previous_quad = Some(ctrl); + self.previous_cubic = None; + Some(KurboPathEl::QuadTo(ctrl, to)) + } + PathEl::SmoothQuadTo(cubics) => { + let (to, rest) = cubics.split(); + let ctrl = self.get_smooth_quad_ctrl(); + + self.current_point = *to; + self.state = IterState::SmoothQuadTo { + transform: false, + rest, + }; + self.previous_quad = Some(ctrl); + self.previous_cubic = None; + Some(KurboPathEl::QuadTo(ctrl, *to)) + } + PathEl::SmoothQuadToRel(cubics) => { + let (to, rest) = cubics.split(); + let to = self.transform() * *to; + let ctrl = self.get_smooth_quad_ctrl(); + + self.current_point = to; + self.state = IterState::SmoothQuadTo { + transform: true, + rest, + }; + self.previous_quad = Some(ctrl); + self.previous_cubic = None; + Some(KurboPathEl::QuadTo(ctrl, to)) + } + PathEl::EllipticArc(_) => todo!(), + PathEl::EllipticArcRel(_) => todo!(), + PathEl::Bearing(bearing) => { + self.current_bearing = bearing.rem_euclid(2. * PI); + None + } + PathEl::BearingRel(bearing_rel) => { + self.current_bearing = (self.current_bearing + bearing_rel).rem_euclid(2. * PI); + None + } + } + } + + fn handle_state(&mut self, state: IterState<'iter>) -> Option { + match state { + IterState::None => None, + IterState::LineTo { transform, rest } => { + let Some((first, rest)) = rest.split_first() else { + return None; + }; + let mut first = *first; + + if transform { + first = self.transform() * first; + } + self.state = IterState::LineTo { transform, rest }; + self.current_point = first; + Some(KurboPathEl::LineTo(first)) + } + IterState::HorizTo { transform, rest } => { + let Some((first, rest)) = rest.split_first() else { + return None; + }; + let mut first = Point::new(*first, 0.); + + if transform { + first = self.transform() * first; + } + self.state = IterState::HorizTo { transform, rest }; + self.current_point = first; + Some(KurboPathEl::LineTo(first)) + } + IterState::VertTo { transform, rest } => { + let Some((first, rest)) = rest.split_first() else { + return None; + }; + let mut first = Point::new(0., *first); + + if transform { + first = self.transform() * first; + } + self.state = IterState::VertTo { transform, rest }; + self.current_point = first; + Some(KurboPathEl::LineTo(first)) + } + IterState::CubicTo { transform, rest } => { + let Some((CubicTo { ctrl1, ctrl2, to }, rest)) = rest.split_first() else { + return None; + }; + let mut ctrl1 = *ctrl1; + let mut ctrl2 = *ctrl2; + let mut to = *to; + if transform { + let trans = self.transform(); + ctrl1 = trans * ctrl1; + ctrl2 = trans * ctrl2; + to = trans * to; + } + self.current_point = to; + self.state = IterState::CubicTo { transform, rest }; + // no need to set quad as we already did it for the first element + self.previous_cubic = Some(ctrl2); + Some(KurboPathEl::CurveTo(ctrl1, ctrl2, to)) + } + IterState::SmoothCubicTo { transform, rest } => { + let Some((SmoothCubicTo { ctrl2, to }, rest)) = rest.split_first() else { + return None; + }; + let ctrl1 = self.get_smooth_cubic_ctrl1(); + let mut ctrl2 = *ctrl2; + let mut to = *to; + if transform { + let trans = self.transform(); + ctrl2 = trans * ctrl2; + to = trans * to; + } + + self.current_point = to; + self.state = IterState::SmoothCubicTo { transform, rest }; + // no need to set quad as we already did it for the first element + self.previous_cubic = Some(ctrl2); + Some(KurboPathEl::CurveTo(ctrl1, ctrl2, to)) + } + IterState::QuadTo { transform, rest } => { + let Some((QuadTo { ctrl, to }, rest)) = rest.split_first() else { + return None; + }; + let mut ctrl = *ctrl; + let mut to = *to; + if transform { + let trans = self.transform(); + ctrl = trans * ctrl; + to = trans * to; + } + self.current_point = to; + self.state = IterState::QuadTo { transform, rest }; + // no need to set quad as we already did it for the first element + self.previous_quad = Some(ctrl); + Some(KurboPathEl::QuadTo(ctrl, to)) + } + IterState::SmoothQuadTo { transform, rest } => { + let Some((to, rest)) = rest.split_first() else { + return None; + }; + let ctrl = self.get_smooth_quad_ctrl(); + let mut to = *to; + if transform { + let trans = self.transform(); + to = trans * to; + } + + self.current_point = to; + self.state = IterState::SmoothQuadTo { transform, rest }; + // no need to set quad as we already did it for the first element + self.previous_quad = Some(ctrl); + Some(KurboPathEl::QuadTo(ctrl, to)) + } + } + } + + /// Get the transform for the current start position and heading. + fn transform(&self) -> Affine { + // XXX I think this is correct, but not yet 100% + Affine::translate(self.current_point.to_vec2()) * Affine::rotate(self.current_bearing) + } + + fn reset_prev_beziers(&mut self) { + self.previous_cubic = None; + self.previous_quad = None; + } + + /// The ctrl1 of a smooth bezier is the reflection of the previous ctrl2 through the previous + /// endpoint, or just the previous endpoint if the previous segment wasn't a cubic. + fn get_smooth_cubic_ctrl1(&self) -> Point { + let Some(ctrl2) = self.previous_cubic else { + return self.current_point; + }; + Point { + x: 2. * self.current_point.x - ctrl2.x, + y: 2. * self.current_point.y - ctrl2.y, + } + } + + /// The ctrl of a smooth quad is the reflection of the previous ctrl through the previous + /// endpoint, or just the previous endpoint if the previous segment wasn't a quad. + fn get_smooth_quad_ctrl(&self) -> Point { + let Some(ctrl) = self.previous_quad else { + return self.current_point; + }; + Point { + x: 2. * self.current_point.x - ctrl.x, + y: 2. * self.current_point.y - ctrl.y, + } + } } impl<'iter> Iterator for PathElementsIter<'iter> { type Item = KurboPathEl; fn next(&mut self) -> Option { - todo!() + loop { + // Remember to but the state back if necessary. + let existing_state = mem::replace(&mut self.state, IterState::None); + if let Some(el) = self.handle_state(existing_state) { + return Some(el); + } + let Some((first, rest)) = self.path.split_first() else { + break; + }; + self.path = rest; + if let Some(kurbo_path) = self.next_el(first) { + return Some(kurbo_path); + } + } + None } } @@ -454,13 +995,19 @@ mod one_vec { /// A vector that has at least 1 element. /// /// It stores the first element on the stack, and the rest on the heap. + /// + /// You can create a new `OneVec` either from the first element ([`single`][OneVec::single]) or + /// from the `TryFrom>` implementation. #[derive(Debug, Clone)] pub struct OneVec { + /// The first, required element in the `OneVec`. pub first: T, + /// The second and subsequent elements in this `OneVec` (all optional). pub rest: Vec, } impl OneVec { + /// Create a `OneVec` from a single element. pub fn single(val: T) -> Self { Self { first: val, @@ -468,10 +1015,50 @@ mod one_vec { } } + /// Iterate over the values in this `OneVec`. + /// + /// The iterator is statically guaranteed to produce at least one element. pub fn iter(&self) -> iter::Chain, slice::Iter<'_, T>> { self.into_iter() } + /// Get the element at the given index. + /// + /// If the index is `0`, then this is guaranteed to return `Some`. + pub fn get(&self, idx: usize) -> Option<&T> { + if idx == 0 { + Some(&self.first) + } else { + self.rest.get(idx - 1) + } + } + + /// Get the element at the given index. + /// + /// If the index is `0`, then this is guaranteed to return `Some`. + pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> { + if idx == 0 { + Some(&mut self.first) + } else { + self.rest.get_mut(idx - 1) + } + } + + /// Get the first element. + pub fn first(&self) -> &T { + &self.first + } + + /// Get the first element. + pub fn first_mut(&mut self) -> &mut T { + &mut self.first + } + + /// Splits the `OneVec` into the first element and the rest. + pub fn split(&self) -> (&T, &[T]) { + (&self.first, &self.rest) + } + /// Write out the vector with spaces between each element pub(crate) fn write_spaced( &self, From 53e34d9358225cbf179aec72e3240de2700400b3 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 16 Apr 2023 19:20:49 +0100 Subject: [PATCH 4/5] WIP --- Cargo.toml | 4 +- src/lib.rs | 1 + src/paths/mod.rs | 2 +- src/paths/{svg.rs => svg/mod.rs} | 412 ++++++------------------------ src/paths/svg/onevec.rs | 114 +++++++++ src/paths/svg/string_repr.rs | 420 +++++++++++++++++++++++++++++++ src/svg.rs | 169 +------------ 7 files changed, 630 insertions(+), 492 deletions(-) rename src/paths/{svg.rs => svg/mod.rs} (75%) create mode 100644 src/paths/svg/onevec.rs create mode 100644 src/paths/svg/string_repr.rs diff --git a/Cargo.toml b/Cargo.toml index 5b9f6d80..4c6499a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,15 @@ categories = ["graphics"] features = ["mint", "schemars", "serde"] [features] -default = ["std"] +default = ["std", "svg_path"] std = [] serde = ["smallvec/serde", "arrayvec/serde", "serde1"] +svg_path = ["nom"] [dependencies] smallvec = "1.10.0" anyhow = "1.0.70" +nom = { version = "7.1.3", optional = true } [dependencies.arrayvec] version = "0.7.1" diff --git a/src/lib.rs b/src/lib.rs index bdfcfeb6..3472344a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ mod line; mod mindist; pub mod offset; mod param_curve; +#[cfg(any(feature = "svg_path"))] pub mod paths; mod point; mod quadbez; diff --git a/src/paths/mod.rs b/src/paths/mod.rs index 89a11cb0..727d2ed6 100644 --- a/src/paths/mod.rs +++ b/src/paths/mod.rs @@ -4,5 +4,5 @@ //! accuracy. However, sometimes it is useful to represent paths in different ways, for example //! circular arcs are often used in architecture, and SVG defines a different path model to //! `BezPath`, so they cannot be used interchangeably. - +#[cfg(feature = "svg_path")] pub mod svg; diff --git a/src/paths/svg.rs b/src/paths/svg/mod.rs similarity index 75% rename from src/paths/svg.rs rename to src/paths/svg/mod.rs index 31044423..c7203849 100644 --- a/src/paths/svg.rs +++ b/src/paths/svg/mod.rs @@ -1,11 +1,17 @@ //! This module provides type [`Path`] representing SVG path data, and associated types. -use crate::{Affine, PathEl as KurboPathEl, Point, Shape, Vec2}; -use anyhow::{anyhow, Result}; -use std::{f64::consts::PI, fmt, io, iter, mem, ops::Deref, slice, vec}; +//! +//! In addition to providing SVG path support, this path type serves as a proof-of-concept and +//! template for custom path types that implement [`kurbo::Shape`], thereby allowing it to be drawn +//! by any rendering engine that supports [`BezPath`][kurbo::BezPath] rendering. +use crate::{Affine, PathEl as KurboPathEl, Point, Rect, Shape, Vec2}; +use anyhow::{anyhow, ensure, Result}; +use std::{f64::consts::PI, io, mem, ops::Deref, vec}; -pub use self::one_vec::*; +mod onevec; +mod string_repr; -type OneIter<'a, T> = iter::Chain, slice::Iter<'a, T>>; +use self::onevec::*; +pub use self::string_repr::{parse, SvgParseError}; /// An SVG path /// @@ -19,6 +25,8 @@ type OneIter<'a, T> = iter::Chain, slice::Iter<'a, T>>; pub struct Path { /// The elements that make up this path. elements: Vec, + /// A cache to record we've seen a moveto + has_moveto: bool, } /// An SVG path element @@ -131,16 +139,17 @@ pub enum PathEl { /// `A`: Draw an elliptical arc. /// /// See the documentation for [`Arc`] for more details. - EllipticArc(OneVec), + EllipticArc(OneVec), /// `a`: Draw an elliptical arc. /// /// See the documentation for [`Arc`] for more details. The points are interpreted taking into /// account the bearing and previous position, as detailed in the [type documentation][PathEl]. /// In particular, this affects the x-axis rotation (which will be the sum of the given x-axis /// rotation and the bearing). - EllipticArcRel(OneVec), + EllipticArcRel(OneVec), // TODO catmull-rom. These curves look interesting but I don't know anything about them and - // so it makes sense to tackle them later. + // so it makes sense to tackle them later. Update: they are based on defining the location and + // derivative at a set of points. /// Set the bearing. /// /// This overwrites the existing bearing. This is another place where this implementation is @@ -201,7 +210,7 @@ pub struct QuadTo { #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Arc { +pub struct ArcTo { /// The arc's end point. pub to: Point, /// The arc's radii, where the vector's x-component is the radius in the @@ -218,7 +227,35 @@ pub struct Arc { impl Path { /// Create a new path object. pub fn new() -> Self { - Self { elements: vec![] } + Self { + elements: vec![], + has_moveto: false, + } + } + + /// Create a new path object with space for `length` path elements. + pub fn with_capacity(length: usize) -> Self { + Self { + elements: Vec::with_capacity(length), + has_moveto: false, + } + } + + /// Create a new path object from a vec of elements. + /// + /// This function will return an error if the resulting path would be invalid. + pub fn from_elements(elements: Vec) -> Result { + let mut this = Self { + elements, + has_moveto: false, + }; + this.validate()?; + Ok(this) + } + + /// Try to parse a string as an SVG path. + pub fn parse(input: &str) -> Result { + parse(input) } /// Push an element onto the end of an array. @@ -226,22 +263,18 @@ impl Path { /// All elements apart from `MoveTo` and `Bearing` must be preceeded by a `MoveTo`. pub fn push(&mut self, el: PathEl) -> Result<()> { // bearings and moveto are always allowed - if let PathEl::MoveTo(_) = &el { + if matches!(&el, PathEl::MoveTo(_)) { self.elements.push(el); + self.has_moveto = true; return Ok(()); } - if let PathEl::Bearing(_) = &el { + if matches!(&el, PathEl::Bearing(_)) { self.elements.push(el); return Ok(()); } // other elements are only allowed if they come after a moveto - if self - .elements - .iter() - .find(|el| matches!(*el, PathEl::MoveTo(_))) - .is_none() - { + if !self.has_moveto { return Err(anyhow!( "all line and curve elements must be preceeded by a moveto" )); @@ -251,19 +284,6 @@ impl Path { Ok(()) } - /// Write out a text representation of the string. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut iter = self.elements.iter(); - if let Some(el) = iter.next() { - el.write(f)?; - } - for el in iter { - write!(f, " ")?; - el.write(f)?; - } - Ok(()) - } - /// Write out the text representation of this path to anything implementing `io::Write`. /// /// `Path` also implements [`Display`][std::fmt::Display], which can be used when you need an @@ -277,34 +297,32 @@ impl Path { } /// Returns an error if the path is invalid - fn validate(&self) -> Result<()> { - let move_idx = self - .elements - .iter() - .enumerate() - .find(|(_, el)| matches!(el, PathEl::MoveTo(_) | PathEl::MoveToRel(_))) - .map(|(idx, _)| idx); - let path_idx = self - .elements - .iter() - .enumerate() - .find(|(_, el)| { - !matches!( - el, - PathEl::MoveTo(_) - | PathEl::MoveToRel(_) - | PathEl::Bearing(_) - | PathEl::BearingRel(_) - ) - }) - .map(|(idx, _)| idx); - match (move_idx, path_idx) { - (None, Some(idx)) => Err(anyhow!("First path at index {idx} before first move")), - (Some(move_idx), Some(path_idx)) if move_idx > path_idx => Err(anyhow!( - "First path at index {path_idx} before first move at index {move_idx}" - )), - _ => Ok(()), + /// + /// Used to check invariants when a path is constructed from a list of path elemenets. + fn validate(&mut self) -> Result<()> { + let Path { + ref mut has_moveto, + ref mut elements, + } = self; + for (idx, el) in elements.into_iter().enumerate() { + match el { + PathEl::MoveTo(_) => { + *has_moveto = true; + } + PathEl::Bearing(_) => { + // fall through + } + _ => { + // all other elements must be preceeded by moveto + ensure!( + *has_moveto, + "path element {el:?} at index {idx} was not preceeded by a \ + `PathEl::MoveTo(_)`" + ) + } + } } + Ok(()) } } @@ -320,15 +338,7 @@ impl TryFrom> for Path { type Error = anyhow::Error; fn try_from(value: Vec) -> Result { - let path = Path { elements: value }; - path.validate()?; - Ok(path) - } -} - -impl fmt::Display for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.fmt(f) + Path::from_elements(value) } } @@ -353,156 +363,17 @@ impl Shape for Path { todo!() } - fn perimeter(&self, accuracy: f64) -> f64 { - self.elements.iter().map(PathEl::length).sum() - } - - fn winding(&self, pt: Point) -> i32 { + fn perimeter(&self, _accuracy: f64) -> f64 { todo!() } - fn bounding_box(&self) -> crate::Rect { + fn winding(&self, _pt: Point) -> i32 { todo!() } -} -impl PathEl { - fn length(&self) -> f64 { + fn bounding_box(&self) -> Rect { todo!() } - - fn write(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - PathEl::MoveTo(points) => { - write!(f, "M")?; - points.write_spaced(write_point, f)?; - } - PathEl::MoveToRel(points) => { - write!(f, "m")?; - points.write_spaced(write_point, f)?; - } - PathEl::ClosePath => { - write!(f, "Z")?; - } - PathEl::LineTo(points) => { - write!(f, "L")?; - points.write_spaced(write_point, f)?; - } - PathEl::LineToRel(points) => { - write!(f, "l")?; - points.write_spaced(write_point, f)?; - } - PathEl::Horiz(amts) => { - write!(f, "H")?; - amts.write_spaced(|v, f| write!(f, "{}", v), f)?; - } - PathEl::HorizRel(amts) => { - write!(f, "h")?; - amts.write_spaced(|v, f| write!(f, "{}", v), f)?; - } - PathEl::Vert(amts) => { - write!(f, "V")?; - amts.write_spaced(|v, f| write!(f, "{}", v), f)?; - } - PathEl::VertRel(amts) => { - write!(f, "v")?; - amts.write_spaced(|v, f| write!(f, "{}", v), f)?; - } - PathEl::CubicTo(cubic_tos) => { - write!(f, "C")?; - cubic_tos.write_spaced(CubicTo::write_vals, f)?; - } - PathEl::CubicToRel(cubic_tos) => { - write!(f, "c")?; - cubic_tos.write_spaced(CubicTo::write_vals, f)?; - } - PathEl::SmoothCubicTo(cubic_tos) => { - write!(f, "S")?; - cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; - } - PathEl::SmoothCubicToRel(cubic_tos) => { - write!(f, "s")?; - cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; - } - PathEl::QuadTo(quad_tos) => { - write!(f, "Q")?; - quad_tos.write_spaced(QuadTo::write_vals, f)?; - } - PathEl::QuadToRel(quad_tos) => { - write!(f, "q")?; - quad_tos.write_spaced(QuadTo::write_vals, f)?; - } - PathEl::SmoothQuadTo(quad_tos) => { - write!(f, "T")?; - quad_tos.write_spaced(write_point, f)?; - } - PathEl::SmoothQuadToRel(quad_tos) => { - write!(f, "t")?; - quad_tos.write_spaced(write_point, f)?; - } - PathEl::EllipticArc(arcs) => { - write!(f, "A")?; - arcs.write_spaced(Arc::write_vals, f)?; - } - PathEl::EllipticArcRel(arcs) => { - write!(f, "a")?; - arcs.write_spaced(Arc::write_vals, f)?; - } - PathEl::Bearing(bearing) => { - write!(f, "B{bearing}",)?; - } - PathEl::BearingRel(bearing) => { - write!(f, "b{bearing}",)?; - } - } - Ok(()) - } -} - -impl CubicTo { - fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{},{} {},{} {},{}", - self.ctrl1.x, self.ctrl1.y, self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y - ) - } -} - -impl SmoothCubicTo { - fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{},{} {},{}", - self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y - ) - } -} - -impl QuadTo { - fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{},{} {},{}", - self.ctrl.x, self.ctrl.y, self.to.x, self.to.y - ) - } -} - -impl Arc { - fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{},{} {} {},{} {},{}", - self.radii.x, - self.radii.y, - self.x_rotation, - self.large_arc, - self.sweep, - self.to.x, - self.to.y - ) - } } /// An iterator over the path elements of an SVG path. @@ -513,6 +384,7 @@ pub struct PathElementsIter<'iter> { /// The path we are traversing. path: &'iter [PathEl], /// Tolerance parameter + #[allow(unused)] tolerance: f64, /// The start point of the current sub-path (this resets for every `MoveTo`). path_start_point: Point, @@ -973,6 +845,7 @@ impl<'iter> Iterator for PathElementsIter<'iter> { return Some(el); } let Some((first, rest)) = self.path.split_first() else { + // This break is hit if we have exhaused the vec of svg path els. break; }; self.path = rest; @@ -983,124 +856,3 @@ impl<'iter> Iterator for PathElementsIter<'iter> { None } } - -fn write_point(Point { x, y }: &Point, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{x},{y}") -} - -mod one_vec { - use anyhow::anyhow; - use std::{fmt, iter, slice, vec}; - - /// A vector that has at least 1 element. - /// - /// It stores the first element on the stack, and the rest on the heap. - /// - /// You can create a new `OneVec` either from the first element ([`single`][OneVec::single]) or - /// from the `TryFrom>` implementation. - #[derive(Debug, Clone)] - pub struct OneVec { - /// The first, required element in the `OneVec`. - pub first: T, - /// The second and subsequent elements in this `OneVec` (all optional). - pub rest: Vec, - } - - impl OneVec { - /// Create a `OneVec` from a single element. - pub fn single(val: T) -> Self { - Self { - first: val, - rest: vec![], - } - } - - /// Iterate over the values in this `OneVec`. - /// - /// The iterator is statically guaranteed to produce at least one element. - pub fn iter(&self) -> iter::Chain, slice::Iter<'_, T>> { - self.into_iter() - } - - /// Get the element at the given index. - /// - /// If the index is `0`, then this is guaranteed to return `Some`. - pub fn get(&self, idx: usize) -> Option<&T> { - if idx == 0 { - Some(&self.first) - } else { - self.rest.get(idx - 1) - } - } - - /// Get the element at the given index. - /// - /// If the index is `0`, then this is guaranteed to return `Some`. - pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> { - if idx == 0 { - Some(&mut self.first) - } else { - self.rest.get_mut(idx - 1) - } - } - - /// Get the first element. - pub fn first(&self) -> &T { - &self.first - } - - /// Get the first element. - pub fn first_mut(&mut self) -> &mut T { - &mut self.first - } - - /// Splits the `OneVec` into the first element and the rest. - pub fn split(&self) -> (&T, &[T]) { - (&self.first, &self.rest) - } - - /// Write out the vector with spaces between each element - pub(crate) fn write_spaced( - &self, - mut cb: impl FnMut(&T, &mut fmt::Formatter) -> fmt::Result, - f: &mut fmt::Formatter, - ) -> fmt::Result { - cb(&self.first, f)?; - for v in &self.rest { - cb(v, f)?; - } - Ok(()) - } - } - - impl TryFrom> for OneVec { - type Error = anyhow::Error; - fn try_from(mut v: Vec) -> Result { - // Annoyingly the `Vec::remove` method can panic, so we have to check - // the vec is non-empty - if v.is_empty() { - return Err(anyhow!("vector must not be empty")); - } - let first = v.remove(0); - Ok(OneVec { first, rest: v }) - } - } - - impl<'a, T> IntoIterator for &'a OneVec { - type IntoIter = iter::Chain, slice::Iter<'a, T>>; - type Item = &'a T; - - fn into_iter(self) -> Self::IntoIter { - iter::once(&self.first).chain(&self.rest) - } - } - - impl IntoIterator for OneVec { - type IntoIter = iter::Chain, vec::IntoIter>; - type Item = T; - - fn into_iter(self) -> Self::IntoIter { - iter::once(self.first).chain(self.rest) - } - } -} diff --git a/src/paths/svg/onevec.rs b/src/paths/svg/onevec.rs new file mode 100644 index 00000000..caa8a409 --- /dev/null +++ b/src/paths/svg/onevec.rs @@ -0,0 +1,114 @@ +use anyhow::anyhow; +use std::{fmt, iter, slice, vec}; + +/// A vector that has at least 1 element. +/// +/// It stores the first element on the stack, and the rest on the heap. +/// +/// You can create a new `OneVec` either from the first element ([`single`][OneVec::single]) or +/// from the `TryFrom>` implementation. +#[derive(Debug, Clone)] +pub struct OneVec { + /// The first, required element in the `OneVec`. + pub first: T, + /// The second and subsequent elements in this `OneVec` (all optional). + pub rest: Vec, +} + +impl OneVec { + /// Create a `OneVec` from a single element. + pub fn single(val: T) -> Self { + Self { + first: val, + rest: vec![], + } + } + + /// Iterate over the values in this `OneVec`. + /// + /// The iterator is statically guaranteed to produce at least one element. + pub fn iter(&self) -> iter::Chain, slice::Iter<'_, T>> { + self.into_iter() + } + + /// Get the element at the given index. + /// + /// If the index is `0`, then this is guaranteed to return `Some`. + pub fn get(&self, idx: usize) -> Option<&T> { + if idx == 0 { + Some(&self.first) + } else { + self.rest.get(idx - 1) + } + } + + /// Get the element at the given index. + /// + /// If the index is `0`, then this is guaranteed to return `Some`. + pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> { + if idx == 0 { + Some(&mut self.first) + } else { + self.rest.get_mut(idx - 1) + } + } + + /// Get the first element. + pub fn first(&self) -> &T { + &self.first + } + + /// Get the first element. + pub fn first_mut(&mut self) -> &mut T { + &mut self.first + } + + /// Splits the `OneVec` into the first element and the rest. + pub fn split(&self) -> (&T, &[T]) { + (&self.first, &self.rest) + } + + /// Write out the vector with spaces between each element + pub(crate) fn write_spaced( + &self, + mut cb: impl FnMut(&T, &mut fmt::Formatter) -> fmt::Result, + f: &mut fmt::Formatter, + ) -> fmt::Result { + cb(&self.first, f)?; + for v in &self.rest { + cb(v, f)?; + } + Ok(()) + } +} + +impl TryFrom> for OneVec { + type Error = anyhow::Error; + fn try_from(mut v: Vec) -> Result { + // Annoyingly the `Vec::remove` method can panic, so we have to check + // the vec is non-empty + if v.is_empty() { + return Err(anyhow!("vector must not be empty")); + } + let first = v.remove(0); + Ok(OneVec { first, rest: v }) + } +} + +impl<'a, T> IntoIterator for &'a OneVec { + type IntoIter = iter::Chain, slice::Iter<'a, T>>; + type Item = &'a T; + + fn into_iter(self) -> Self::IntoIter { + iter::once(&self.first).chain(&self.rest) + } +} + +impl IntoIterator for OneVec { + type IntoIter = iter::Chain, vec::IntoIter>; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + iter::once(self.first).chain(self.rest) + } +} diff --git a/src/paths/svg/string_repr.rs b/src/paths/svg/string_repr.rs new file mode 100644 index 00000000..ee96e1fc --- /dev/null +++ b/src/paths/svg/string_repr.rs @@ -0,0 +1,420 @@ +//! This module handles converting to/from the string representation of an SVG path. +use self::lexer::{Lexer, Token}; +use crate::{ + paths::svg::{ArcTo, CubicTo, Path, PathEl, QuadTo, SmoothCubicTo}, + Point, +}; +use std::{error::Error as StdError, fmt}; + +// parse + +/// Try to parse the input as an SVG path. +pub fn parse(input: &str) -> Result { + todo!() +} + +/// An error which can be returned when parsing an SVG. +#[derive(Debug)] +pub enum SvgParseError { + /// A number was expected. + Wrong, + /// The input string ended while still expecting input. + UnexpectedEof, + /// Encountered an unknown command letter. + UnknownCommand(char), +} + +impl fmt::Display for SvgParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SvgParseError::Wrong => write!(f, "Unable to parse a number"), + SvgParseError::UnexpectedEof => write!(f, "Unexpected EOF"), + SvgParseError::UnknownCommand(letter) => write!(f, "Unknown command, \"{letter}\""), + } + } +} + +impl StdError for SvgParseError {} + +mod parser { + use super::{Path, PathEl}; + use crate::Point; + use nom::{ + branch::alt, + bytes::complete::{tag, take_till}, + combinator::{map, opt, recognize, value}, + number::complete::double, + sequence::tuple, + IResult, + }; + + /// top-level parser for svg path + pub fn path(input: &str) -> IResult<&str, Path> { + let mut path = Path::new(); + + let (input, _) = whitespace(input)?; + let (input, _) = moveto(&mut path, input)?; + todo!() + } + + fn command(input: &str) -> IResult<&str, PathEl> { + todo!() + } + + fn moveto<'src>(path: &mut Path, input: &'src str) -> IResult<&'src str, ()> { + // command + let (input, _) = whitespace(input)?; + let (input, rel) = alt((value(true, tag("M")), value(false, tag("m"))))(input)?; + + // one or more coordinate pairs + + // optional closepath + let (input, _) = whitespace(input)?; + let (input, close) = opt(closepath)(input)?; + let close = close.is_some(); + + // now we're at the end, add all this to the path + todo!() + } + + fn closepath(input: &str) -> IResult<&str, &str> { + alt((tag("z"), tag("Z")))(input) + } + + fn coordinate_pair(input: &str) -> IResult<&str, Point> { + let (input, x) = coordinate(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, y) = coordinate(input)?; + Ok((input, Point { x, y })) + } + + fn coordinate(input: &str) -> IResult<&str, f64> { + double(input) + } + + fn flag(input: &str) -> IResult<&str, bool> { + alt((value(true, tag("1")), value(false, tag("0"))))(input) + } + + fn comma_or_ws(input: &str) -> IResult<&str, &str> { + fn inner(input: &str) -> IResult<&str, ()> { + let (input, _) = whitespace(input)?; + let (input, _) = opt(tag(","))(input)?; + let (input, _) = whitespace(input)?; + Ok((input, ())) + } + recognize(inner)(input) + } + + fn whitespace(input: &str) -> IResult<&str, &str> { + take_till(|ch| matches!(ch, ' ' | '\u{9}' | '\u{a}' | '\u{c}' | '\u{d}'))(input) + } +} + +mod lexer { + use super::SvgParseError; + use crate::Point; + + type Result = std::result::Result; + + /// An input token + /// + /// Here we are more flexible than the specification. Some parts of the specification require + /// either a comma or space between numbers, but here we allow this to be omitted. I think the + /// specification encourages the behavior of this parser, but I find it unclear on this point. + pub enum Token<'a> { + Number(TokenData<'a, f64>), + Command(TokenData<'a, char>), + } + + pub struct TokenData<'a, T> { + value: T, + src: &'a str, + } + + pub struct Lexer<'a> { + src: &'a str, + } + + impl<'a> Lexer<'a> { + fn new(src: &str) -> Lexer { + Lexer { src } + } + + fn get_command(&mut self) -> Result>> { + todo!() + } + + fn get_number(&mut self) -> Result> { + todo!() + } + + fn skip_ws(&self) { + let mut input = self.src; + while let Some(c) = input.chars().next() { + if !(c == ' ' // '\u{20}' + || c == '\u{9}' + || c == '\u{a}' + || c == '\u{c}' + || c == '\u{d}' + || c == ',') + { + break; + } + input = &input[1..]; + } + } + + fn get_cmd(&mut self, last_cmd: u8) -> Option { + self.skip_ws(); + if let Some(c) = self.get_byte() { + if c.is_ascii_lowercase() || c.is_ascii_uppercase() { + return Some(c); + } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) { + // Plausible number start + self.unget(); + return Some(last_cmd); + } else { + self.unget(); + } + } + None + } + + fn try_number(&self) -> Result<(&str, f64), SvgParseError> { + let (input, _) = self.skip_ws(); + let start = self.ix; + let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; + if !(c == b'-' || c == b'+') { + self.unget(); + } + let mut digit_count = 0; + let mut seen_period = false; + while let Some(c) = self.get_byte() { + if c.is_ascii_digit() { + digit_count += 1; + } else if c == b'.' && !seen_period { + seen_period = true; + } else { + self.unget(); + break; + } + } + if let Some(c) = self.get_byte() { + if c == b'e' || c == b'E' { + let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?; + if c == b'-' || c == b'+' { + c = self.get_byte().ok_or(SvgParseError::Wrong)? + } + if c.is_ascii_digit() { + return Err(SvgParseError::Wrong); + } + while let Some(c) = self.get_byte() { + if c.is_ascii_digit() { + self.unget(); + break; + } + } + } else { + self.unget(); + } + } + if digit_count > 0 { + self.data[start..self.ix] + .parse() + .map_err(|_| SvgParseError::Wrong) + } else { + Err(SvgParseError::Wrong) + } + } + + fn get_flag(&mut self) -> Result { + self.skip_ws(); + match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? { + b'0' => Ok(false), + b'1' => Ok(true), + _ => Err(SvgParseError::Wrong), + } + } + + fn get_number_pair(&mut self) -> Result { + let x = self.get_number()?; + self.opt_comma(); + let y = self.get_number()?; + self.opt_comma(); + Ok(Point::new(x, y)) + } + + fn get_maybe_relative(&mut self, cmd: u8) -> Result { + let pt = self.get_number_pair()?; + if cmd.is_ascii_lowercase() { + Ok(self.last_pt + pt.to_vec2()) + } else { + Ok(pt) + } + } + + fn opt_comma(&mut self) { + self.skip_ws(); + if let Some(c) = self.get_byte() { + if c != b',' { + self.unget(); + } + } + } + } +} + +// Stringify + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut iter = self.elements.iter(); + if let Some(el) = iter.next() { + write!(f, "{el}")?; + } + for el in iter { + write!(f, " {el}")?; + } + Ok(()) + } +} + +impl fmt::Display for PathEl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PathEl::MoveTo(points) => { + write!(f, "M")?; + points.write_spaced(write_point, f)?; + } + PathEl::MoveToRel(points) => { + write!(f, "m")?; + points.write_spaced(write_point, f)?; + } + PathEl::ClosePath => { + write!(f, "Z")?; + } + PathEl::LineTo(points) => { + write!(f, "L")?; + points.write_spaced(write_point, f)?; + } + PathEl::LineToRel(points) => { + write!(f, "l")?; + points.write_spaced(write_point, f)?; + } + PathEl::Horiz(amts) => { + write!(f, "H")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::HorizRel(amts) => { + write!(f, "h")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::Vert(amts) => { + write!(f, "V")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::VertRel(amts) => { + write!(f, "v")?; + amts.write_spaced(|v, f| write!(f, "{}", v), f)?; + } + PathEl::CubicTo(cubic_tos) => { + write!(f, "C")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::CubicToRel(cubic_tos) => { + write!(f, "c")?; + cubic_tos.write_spaced(CubicTo::write_vals, f)?; + } + PathEl::SmoothCubicTo(cubic_tos) => { + write!(f, "S")?; + cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; + } + PathEl::SmoothCubicToRel(cubic_tos) => { + write!(f, "s")?; + cubic_tos.write_spaced(SmoothCubicTo::write_vals, f)?; + } + PathEl::QuadTo(quad_tos) => { + write!(f, "Q")?; + quad_tos.write_spaced(QuadTo::write_vals, f)?; + } + PathEl::QuadToRel(quad_tos) => { + write!(f, "q")?; + quad_tos.write_spaced(QuadTo::write_vals, f)?; + } + PathEl::SmoothQuadTo(quad_tos) => { + write!(f, "T")?; + quad_tos.write_spaced(write_point, f)?; + } + PathEl::SmoothQuadToRel(quad_tos) => { + write!(f, "t")?; + quad_tos.write_spaced(write_point, f)?; + } + PathEl::EllipticArc(arcs) => { + write!(f, "A")?; + arcs.write_spaced(ArcTo::write_vals, f)?; + } + PathEl::EllipticArcRel(arcs) => { + write!(f, "a")?; + arcs.write_spaced(ArcTo::write_vals, f)?; + } + PathEl::Bearing(bearing) => { + write!(f, "B{bearing}",)?; + } + PathEl::BearingRel(bearing) => { + write!(f, "b{bearing}",)?; + } + } + Ok(()) + } +} + +impl CubicTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{} {},{}", + self.ctrl1.x, self.ctrl1.y, self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y + ) + } +} + +impl SmoothCubicTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{}", + self.ctrl2.x, self.ctrl2.y, self.to.x, self.to.y + ) + } +} + +impl QuadTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {},{}", + self.ctrl.x, self.ctrl.y, self.to.x, self.to.y + ) + } +} + +impl ArcTo { + fn write_vals(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{},{} {} {},{} {},{}", + self.radii.x, + self.radii.y, + self.x_rotation, + self.large_arc, + self.sweep, + self.to.x, + self.to.y + ) + } +} + +fn write_point(Point { x, y }: &Point, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{x},{y}") +} diff --git a/src/svg.rs b/src/svg.rs index c2fc6b2e..7e0cebec 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -15,6 +15,7 @@ impl BezPath { /// /// The current implementation doesn't take any special care to produce a /// short string (reducing precision, using relative movement). + #[deprecated] pub fn to_svg(&self) -> String { let mut buffer = Vec::new(); self.write_to(&mut buffer).unwrap(); @@ -22,6 +23,7 @@ impl BezPath { } /// Write the SVG representation of this path to the provided buffer. + #[deprecated] pub fn write_to(&self, mut writer: W) -> io::Result<()> { for (i, el) in self.elements().iter().enumerate() { if i > 0 { @@ -48,6 +50,7 @@ impl BezPath { /// This is implemented on a best-effort basis, intended for cases where the /// user controls the source of paths, and is not intended as a replacement /// for a general, robust SVG parser. + #[deprecated] pub fn from_svg(data: &str) -> Result { let mut lexer = SvgLexer::new(data); let mut path = BezPath::new(); @@ -188,169 +191,15 @@ impl BezPath { } } -/// An error which can be returned when parsing an SVG. -#[derive(Debug)] -pub enum SvgParseError { - /// A number was expected. - Wrong, - /// The input string ended while still expecting input. - UnexpectedEof, - /// Encountered an unknown command letter. - UnknownCommand(char), -} - -impl Display for SvgParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - SvgParseError::Wrong => write!(f, "Unable to parse a number"), - SvgParseError::UnexpectedEof => write!(f, "Unexpected EOF"), - SvgParseError::UnknownCommand(letter) => write!(f, "Unknown command, \"{letter}\""), - } - } -} - -impl Error for SvgParseError {} - -struct SvgLexer<'a> { - data: &'a str, - ix: usize, - pub last_pt: Point, -} - -impl<'a> SvgLexer<'a> { - fn new(data: &str) -> SvgLexer { - SvgLexer { - data, - ix: 0, - last_pt: Point::ORIGIN, - } - } - - fn skip_ws(&mut self) { - while let Some(&c) = self.data.as_bytes().get(self.ix) { - if !(c == b' ' || c == 9 || c == 10 || c == 12 || c == 13) { - break; - } - self.ix += 1; - } - } - - fn get_cmd(&mut self, last_cmd: u8) -> Option { - self.skip_ws(); - if let Some(c) = self.get_byte() { - if c.is_ascii_lowercase() || c.is_ascii_uppercase() { - return Some(c); - } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) { - // Plausible number start - self.unget(); - return Some(last_cmd); - } else { - self.unget(); - } - } - None - } - - fn get_byte(&mut self) -> Option { - self.data.as_bytes().get(self.ix).map(|&c| { - self.ix += 1; - c - }) - } - - fn unget(&mut self) { - self.ix -= 1; - } - - fn get_number(&mut self) -> Result { - self.skip_ws(); - let start = self.ix; - let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; - if !(c == b'-' || c == b'+') { - self.unget(); - } - let mut digit_count = 0; - let mut seen_period = false; - while let Some(c) = self.get_byte() { - if c.is_ascii_digit() { - digit_count += 1; - } else if c == b'.' && !seen_period { - seen_period = true; - } else { - self.unget(); - break; - } - } - if let Some(c) = self.get_byte() { - if c == b'e' || c == b'E' { - let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?; - if c == b'-' || c == b'+' { - c = self.get_byte().ok_or(SvgParseError::Wrong)? - } - if c.is_ascii_digit() { - return Err(SvgParseError::Wrong); - } - while let Some(c) = self.get_byte() { - if c.is_ascii_digit() { - self.unget(); - break; - } - } - } else { - self.unget(); - } - } - if digit_count > 0 { - self.data[start..self.ix] - .parse() - .map_err(|_| SvgParseError::Wrong) - } else { - Err(SvgParseError::Wrong) - } - } - - fn get_flag(&mut self) -> Result { - self.skip_ws(); - match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? { - b'0' => Ok(false), - b'1' => Ok(true), - _ => Err(SvgParseError::Wrong), - } - } - - fn get_number_pair(&mut self) -> Result { - let x = self.get_number()?; - self.opt_comma(); - let y = self.get_number()?; - self.opt_comma(); - Ok(Point::new(x, y)) - } - - fn get_maybe_relative(&mut self, cmd: u8) -> Result { - let pt = self.get_number_pair()?; - if cmd.is_ascii_lowercase() { - Ok(self.last_pt + pt.to_vec2()) - } else { - Ok(pt) - } - } - - fn opt_comma(&mut self) { - self.skip_ws(); - if let Some(c) = self.get_byte() { - if c != b',' { - self.unget(); - } - } - } -} - impl SvgArc { /// Checks that arc is actually a straight line. /// + /// `tolerance` is the maximum distance between the arc and the equvalent straight line from + /// the start to the end of the arc. + /// /// In this case, it can be replaced with a LineTo. - pub fn is_straight_line(&self) -> bool { - self.radii.x.abs() <= 1e-5 || self.radii.y.abs() <= 1e-5 || self.from == self.to + pub fn is_straight_line(&self, tolerance: f64) -> bool { + self.radii.x.abs() <= tolerance || self.radii.y.abs() <= tolerance || self.from == self.to } } @@ -360,7 +209,7 @@ impl Arc { /// Returns `None` if `arc` is actually a straight line. pub fn from_svg_arc(arc: &SvgArc) -> Option { // Have to check this first, otherwise `sum_of_sq` will be 0. - if arc.is_straight_line() { + if arc.is_straight_line(1e-6) { return None; } From 434bbd981743bff0bbd5595a6cb00cd708be1074 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 16 Apr 2023 23:11:06 +0100 Subject: [PATCH 5/5] Implement parsing (untested) --- src/paths/svg/onevec.rs | 5 + src/paths/svg/string_repr.rs | 449 ++++++++++++++++++++++------------- 2 files changed, 285 insertions(+), 169 deletions(-) diff --git a/src/paths/svg/onevec.rs b/src/paths/svg/onevec.rs index caa8a409..69d77638 100644 --- a/src/paths/svg/onevec.rs +++ b/src/paths/svg/onevec.rs @@ -24,6 +24,11 @@ impl OneVec { } } + /// Construct `self` directly from a first element and a possibly empty vec. + pub fn from_single_rest(first: T, rest: Vec) -> Self { + Self { first, rest } + } + /// Iterate over the values in this `OneVec`. /// /// The iterator is statically guaranteed to produce at least one element. diff --git a/src/paths/svg/string_repr.rs b/src/paths/svg/string_repr.rs index ee96e1fc..429428df 100644 --- a/src/paths/svg/string_repr.rs +++ b/src/paths/svg/string_repr.rs @@ -1,7 +1,6 @@ //! This module handles converting to/from the string representation of an SVG path. -use self::lexer::{Lexer, Token}; use crate::{ - paths::svg::{ArcTo, CubicTo, Path, PathEl, QuadTo, SmoothCubicTo}, + paths::svg::{ArcTo, CubicTo, OneVec, Path, PathEl, QuadTo, SmoothCubicTo}, Point, }; use std::{error::Error as StdError, fmt}; @@ -10,7 +9,10 @@ use std::{error::Error as StdError, fmt}; /// Try to parse the input as an SVG path. pub fn parse(input: &str) -> Result { - todo!() + match parser::path(input) { + Ok((_, v)) => Ok(v), + Err(_) => Err(SvgParseError::Wrong), + } } /// An error which can be returned when parsing an SVG. @@ -37,231 +39,340 @@ impl fmt::Display for SvgParseError { impl StdError for SvgParseError {} mod parser { - use super::{Path, PathEl}; - use crate::Point; + use super::{ArcTo, CubicTo, OneVec, Path, PathEl, QuadTo, SmoothCubicTo}; + use crate::{Point, Vec2}; use nom::{ branch::alt, bytes::complete::{tag, take_till}, combinator::{map, opt, recognize, value}, + multi::{fold_many1, many0}, number::complete::double, sequence::tuple, IResult, }; + use std::f64::consts::PI; /// top-level parser for svg path pub fn path(input: &str) -> IResult<&str, Path> { let mut path = Path::new(); let (input, _) = whitespace(input)?; - let (input, _) = moveto(&mut path, input)?; - todo!() + let (input, el) = moveto(input)?; + path.push(el).unwrap(); // must be a moveto so panic unreachable + let (mut input, _) = whitespace(input)?; + + while !input.is_empty() { + let (input_, el) = command(input)?; + input = input_; + path.push(el).unwrap(); + let (input_, _) = whitespace(input)?; + input = input_; + } + Ok((input, path)) } fn command(input: &str) -> IResult<&str, PathEl> { - todo!() + alt(( + moveto, + closepath, + lineto, + horizontal_lineto, + vertical_lineto, + curveto, + smooth_curveto, + quadto, + smooth_quadto, + arcto, + bearing, + ))(input) } - fn moveto<'src>(path: &mut Path, input: &'src str) -> IResult<&'src str, ()> { + fn moveto<'src>(input: &'src str) -> IResult<&'src str, PathEl> { // command + let (input, rel) = alt((value(false, tag("M")), value(true, tag("m"))))(input)?; + + // one or more coordinate pairs let (input, _) = whitespace(input)?; - let (input, rel) = alt((value(true, tag("M")), value(false, tag("m"))))(input)?; + let (input, points) = coordinate_pair_sequence(input)?; + Ok(( + input, + if rel { + PathEl::MoveToRel(points) + } else { + PathEl::MoveTo(points) + }, + )) + } + + fn closepath(input: &str) -> IResult<&str, PathEl> { + value(PathEl::ClosePath, alt((tag("z"), tag("Z"))))(input) + } + + fn lineto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("L")), value(true, tag("l"))))(input)?; // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, points) = coordinate_pair_sequence(input)?; + Ok(( + input, + if rel { + PathEl::LineToRel(points) + } else { + PathEl::LineTo(points) + }, + )) + } - // optional closepath + fn horizontal_lineto(input: &str) -> IResult<&str, PathEl> { + // command let (input, _) = whitespace(input)?; - let (input, close) = opt(closepath)(input)?; - let close = close.is_some(); + let (input, rel) = alt((value(false, tag("H")), value(true, tag("h"))))(input)?; - // now we're at the end, add all this to the path - todo!() + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, points) = coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::HorizRel(points) + } else { + PathEl::Horiz(points) + }, + )) } - fn closepath(input: &str) -> IResult<&str, &str> { - alt((tag("z"), tag("Z")))(input) + fn vertical_lineto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("V")), value(true, tag("v"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, points) = coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::VertRel(points) + } else { + PathEl::Vert(points) + }, + )) } - fn coordinate_pair(input: &str) -> IResult<&str, Point> { - let (input, x) = coordinate(input)?; - let (input, _) = comma_or_ws(input)?; - let (input, y) = coordinate(input)?; - Ok((input, Point { x, y })) + fn curveto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("C")), value(true, tag("c"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, curves) = curveto_coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::CubicToRel(curves) + } else { + PathEl::CubicTo(curves) + }, + )) } - fn coordinate(input: &str) -> IResult<&str, f64> { - double(input) + fn smooth_curveto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("S")), value(true, tag("s"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, curves) = smooth_curveto_coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::SmoothCubicToRel(curves) + } else { + PathEl::SmoothCubicTo(curves) + }, + )) } - fn flag(input: &str) -> IResult<&str, bool> { - alt((value(true, tag("1")), value(false, tag("0"))))(input) + fn quadto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("Q")), value(true, tag("q"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, curves) = quadto_coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::QuadToRel(curves) + } else { + PathEl::QuadTo(curves) + }, + )) } - fn comma_or_ws(input: &str) -> IResult<&str, &str> { - fn inner(input: &str) -> IResult<&str, ()> { - let (input, _) = whitespace(input)?; - let (input, _) = opt(tag(","))(input)?; - let (input, _) = whitespace(input)?; - Ok((input, ())) - } - recognize(inner)(input) + fn smooth_quadto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("T")), value(true, tag("t"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, points) = coordinate_pair_sequence(input)?; + Ok(( + input, + if rel { + PathEl::SmoothQuadToRel(points) + } else { + PathEl::SmoothQuadTo(points) + }, + )) } - fn whitespace(input: &str) -> IResult<&str, &str> { - take_till(|ch| matches!(ch, ' ' | '\u{9}' | '\u{a}' | '\u{c}' | '\u{d}'))(input) + fn arcto(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("A")), value(true, tag("a"))))(input)?; + + // one or more coordinate pairs + let (input, _) = whitespace(input)?; + let (input, arcs) = arcto_coordinate_sequence(input)?; + Ok(( + input, + if rel { + PathEl::EllipticArcRel(arcs) + } else { + PathEl::EllipticArc(arcs) + }, + )) } -} -mod lexer { - use super::SvgParseError; - use crate::Point; + fn bearing(input: &str) -> IResult<&str, PathEl> { + // command + let (input, rel) = alt((value(false, tag("B")), value(true, tag("b"))))(input)?; + let (input, _) = whitespace(input)?; + let folder = if rel { + |acc: f64, value: f64| (acc + value).rem_euclid(2. * PI) + } else { + |_, value| value + }; + let (input, value) = fold_many1(double, || 0., folder)(input)?; + Ok(( + input, + if rel { + PathEl::BearingRel(value) + } else { + PathEl::Bearing(value) + }, + )) + } - type Result = std::result::Result; + fn arcto_coordinate_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(arcto_coordinates)(input) + } - /// An input token - /// - /// Here we are more flexible than the specification. Some parts of the specification require - /// either a comma or space between numbers, but here we allow this to be omitted. I think the - /// specification encourages the behavior of this parser, but I find it unclear on this point. - pub enum Token<'a> { - Number(TokenData<'a, f64>), - Command(TokenData<'a, char>), + fn quadto_coordinate_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(quadto_coordinates)(input) } - pub struct TokenData<'a, T> { - value: T, - src: &'a str, + fn smooth_curveto_coordinate_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(smooth_curveto_coordinates)(input) } - pub struct Lexer<'a> { - src: &'a str, + fn curveto_coordinate_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(curveto_coordinates)(input) } - impl<'a> Lexer<'a> { - fn new(src: &str) -> Lexer { - Lexer { src } - } + fn coordinate_pair_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(coordinate_pair)(input) + } - fn get_command(&mut self) -> Result>> { - todo!() - } + fn coordinate_sequence(input: &str) -> IResult<&str, OneVec> { + sequence(coordinate)(input) + } - fn get_number(&mut self) -> Result> { - todo!() + fn sequence( + mut inner: impl FnMut(&str) -> IResult<&str, T>, + ) -> impl FnMut(&str) -> IResult<&str, OneVec> { + move |input| { + let (input, first) = inner(input)?; + let (input, rest) = many0(map(tuple((comma_or_ws, &mut inner)), |(_, p)| p))(input)?; + Ok((input, OneVec::from_single_rest(first, rest))) } + } - fn skip_ws(&self) { - let mut input = self.src; - while let Some(c) = input.chars().next() { - if !(c == ' ' // '\u{20}' - || c == '\u{9}' - || c == '\u{a}' - || c == '\u{c}' - || c == '\u{d}' - || c == ',') - { - break; - } - input = &input[1..]; - } - } + fn arcto_coordinates(input: &str) -> IResult<&str, ArcTo> { + let (input, rx) = double(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, ry) = double(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, x_rotation) = double(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, large_arc) = flag(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, sweep) = flag(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, to) = coordinate_pair(input)?; + Ok(( + input, + ArcTo { + to, + radii: Vec2::new(rx, ry), + x_rotation, + large_arc, + sweep, + }, + )) + } - fn get_cmd(&mut self, last_cmd: u8) -> Option { - self.skip_ws(); - if let Some(c) = self.get_byte() { - if c.is_ascii_lowercase() || c.is_ascii_uppercase() { - return Some(c); - } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) { - // Plausible number start - self.unget(); - return Some(last_cmd); - } else { - self.unget(); - } - } - None - } + fn quadto_coordinates(input: &str) -> IResult<&str, QuadTo> { + let (input, ctrl) = coordinate_pair(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, to) = coordinate_pair(input)?; + Ok((input, QuadTo { ctrl, to })) + } - fn try_number(&self) -> Result<(&str, f64), SvgParseError> { - let (input, _) = self.skip_ws(); - let start = self.ix; - let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; - if !(c == b'-' || c == b'+') { - self.unget(); - } - let mut digit_count = 0; - let mut seen_period = false; - while let Some(c) = self.get_byte() { - if c.is_ascii_digit() { - digit_count += 1; - } else if c == b'.' && !seen_period { - seen_period = true; - } else { - self.unget(); - break; - } - } - if let Some(c) = self.get_byte() { - if c == b'e' || c == b'E' { - let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?; - if c == b'-' || c == b'+' { - c = self.get_byte().ok_or(SvgParseError::Wrong)? - } - if c.is_ascii_digit() { - return Err(SvgParseError::Wrong); - } - while let Some(c) = self.get_byte() { - if c.is_ascii_digit() { - self.unget(); - break; - } - } - } else { - self.unget(); - } - } - if digit_count > 0 { - self.data[start..self.ix] - .parse() - .map_err(|_| SvgParseError::Wrong) - } else { - Err(SvgParseError::Wrong) - } - } + fn smooth_curveto_coordinates(input: &str) -> IResult<&str, SmoothCubicTo> { + let (input, ctrl2) = coordinate_pair(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, to) = coordinate_pair(input)?; + Ok((input, SmoothCubicTo { ctrl2, to })) + } - fn get_flag(&mut self) -> Result { - self.skip_ws(); - match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? { - b'0' => Ok(false), - b'1' => Ok(true), - _ => Err(SvgParseError::Wrong), - } - } + fn curveto_coordinates(input: &str) -> IResult<&str, CubicTo> { + let (input, ctrl1) = coordinate_pair(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, ctrl2) = coordinate_pair(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, to) = coordinate_pair(input)?; + Ok((input, CubicTo { ctrl1, ctrl2, to })) + } - fn get_number_pair(&mut self) -> Result { - let x = self.get_number()?; - self.opt_comma(); - let y = self.get_number()?; - self.opt_comma(); - Ok(Point::new(x, y)) - } + fn coordinate_pair(input: &str) -> IResult<&str, Point> { + let (input, x) = coordinate(input)?; + let (input, _) = comma_or_ws(input)?; + let (input, y) = coordinate(input)?; + Ok((input, Point { x, y })) + } - fn get_maybe_relative(&mut self, cmd: u8) -> Result { - let pt = self.get_number_pair()?; - if cmd.is_ascii_lowercase() { - Ok(self.last_pt + pt.to_vec2()) - } else { - Ok(pt) - } - } + fn coordinate(input: &str) -> IResult<&str, f64> { + double(input) + } - fn opt_comma(&mut self) { - self.skip_ws(); - if let Some(c) = self.get_byte() { - if c != b',' { - self.unget(); - } - } + fn flag(input: &str) -> IResult<&str, bool> { + alt((value(true, tag("1")), value(false, tag("0"))))(input) + } + + fn comma_or_ws(input: &str) -> IResult<&str, &str> { + fn inner(input: &str) -> IResult<&str, ()> { + let (input, _) = whitespace(input)?; + let (input, _) = opt(tag(","))(input)?; + let (input, _) = whitespace(input)?; + Ok((input, ())) } + recognize(inner)(input) + } + + fn whitespace(input: &str) -> IResult<&str, &str> { + take_till(|ch| matches!(ch, ' ' | '\u{9}' | '\u{a}' | '\u{c}' | '\u{d}'))(input) } }