From 929707327031d332a5092ad460a9700ed47358cd Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Mar 2026 15:33:26 +0000 Subject: [PATCH 1/3] Implement serialized scene playback in the winit example Signed-off-by: Nico Burns --- Cargo.lock | 13 +++++- Cargo.toml | 3 +- examples/winit/Cargo.toml | 1 + examples/winit/src/main.rs | 91 ++++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e280a..41d2d97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,11 +1786,11 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +source = "git+https://github.com/linebender/kurbo?rev=868cd631beee6f787d63113a9eb72e8c005fa33c#868cd631beee6f787d63113a9eb72e8c005fa33c" dependencies = [ "arrayvec", "euclid", + "polycool", "serde", "smallvec", ] @@ -2654,6 +2654,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "polycool" +version = "0.4.0" +source = "git+https://github.com/linebender/kurbo?rev=868cd631beee6f787d63113a9eb72e8c005fa33c#868cd631beee6f787d63113a9eb72e8c005fa33c" +dependencies = [ + "arrayvec", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -4644,6 +4652,7 @@ name = "winit-example" version = "0.1.0" dependencies = [ "anyrender", + "anyrender_serialize", "anyrender_skia", "anyrender_vello", "anyrender_vello_cpu", diff --git a/Cargo.toml b/Cargo.toml index 7b5a080..47dbd74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,8 @@ pollster = "0.4" # Dev-dependencies winit = { version = "0.30.2", features = ["rwh_06"] } -# [patch.crates-io] +[patch.crates-io] +kurbo = { git = "https://github.com/linebender/kurbo", rev = "868cd631beee6f787d63113a9eb72e8c005fa33c" } # vello = { path = "../vello/vello" } # vello_cpu = { path = "../vello/sparse_strips/vello_cpu" } # vello_hybrid = { path = "../vello/sparse_strips/vello_hybrid" } diff --git a/examples/winit/Cargo.toml b/examples/winit/Cargo.toml index 14eb4b0..f02f7d6 100644 --- a/examples/winit/Cargo.toml +++ b/examples/winit/Cargo.toml @@ -10,6 +10,7 @@ kurbo = { workspace = true } winit = { workspace = true } peniko = { workspace = true } anyrender = { workspace = true } +anyrender_serialize = { workspace = true } anyrender_vello = { workspace = true } anyrender_skia = { workspace = true } anyrender_vello_hybrid = { workspace = true } diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index b651707..fd08d50 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,11 +1,12 @@ -use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; +use anyrender::{NullWindowRenderer, PaintScene, Scene, WindowRenderer}; +use anyrender_serialize::SceneArchive; use anyrender_skia::SkiaWindowRenderer; use anyrender_vello::VelloWindowRenderer; use anyrender_vello_cpu::{PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer}; use anyrender_vello_hybrid::VelloHybridWindowRenderer; use kurbo::{Affine, Circle, Point, Rect, Stroke}; use peniko::{Color, Fill}; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, WindowEvent}, @@ -16,6 +17,7 @@ use winit::{ struct App { render_state: RenderState, + scene: Scene, width: u32, height: u32, } @@ -119,30 +121,6 @@ impl App { } } - fn draw_scene(scene: &mut T, color: Color) { - scene.fill( - Fill::NonZero, - Affine::IDENTITY, - Color::WHITE, - None, - &Rect::new(0.0, 0.0, 50.0, 50.0), - ); - scene.stroke( - &Stroke::new(2.0), - Affine::IDENTITY, - Color::BLACK, - None, - &Rect::new(5.0, 5.0, 35.0, 35.0), - ); - scene.fill( - Fill::NonZero, - Affine::IDENTITY, - color, - None, - &Circle::new(Point::new(20.0, 20.0), 10.0), - ); - } - fn set_backend>( &mut self, mut renderer: R, @@ -204,15 +182,23 @@ impl ApplicationHandler for App { } WindowEvent::RedrawRequested => match renderer { Renderer::Skia(r) => { - r.render(|p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) + } + Renderer::Gpu(r) => { + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) + } + Renderer::Hybrid(r) => { + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) + } + Renderer::Cpu(r) => { + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) } - Renderer::Gpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))), - Renderer::Hybrid(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), - Renderer::Cpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))), Renderer::CpuSoftbuffer(r) => { - r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) + } + Renderer::Null(r) => { + r.render(|painter| painter.append_scene(self.scene.clone(), Affine::IDENTITY)) } - Renderer::Null(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), }, WindowEvent::KeyboardInput { event: @@ -245,8 +231,22 @@ impl ApplicationHandler for App { } fn main() { + let mut args = std::env::args_os(); + let maybe_path = args.nth(1).map(PathBuf::from); + + let scene = if let Some(path) = maybe_path { + let file = std::fs::File::open(&path).expect("File not found"); + let archive = SceneArchive::deserialize(file).expect("Failed to deserialize archive"); + archive + .to_scene() + .expect("Failed to convert archive to scene") + } else { + default_scene() + }; + let mut app = App { render_state: RenderState::Suspended(None), + scene, width: 800, height: 600, }; @@ -256,3 +256,30 @@ fn main() { .run_app(&mut app) .expect("Couldn't run event loop"); } + +fn default_scene() -> Scene { + let mut scene = Scene::new(); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + Color::WHITE, + None, + &Rect::new(0.0, 0.0, 50.0, 50.0), + ); + scene.stroke( + &Stroke::new(2.0), + Affine::IDENTITY, + Color::BLACK, + None, + &Rect::new(5.0, 5.0, 35.0, 35.0), + ); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + Color::from_rgb8(255, 0, 0), + None, + &Circle::new(Point::new(20.0, 20.0), 10.0), + ); + + scene +} From 293f8b766daf74ec8c06f92a741f3f3fd4335d70 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Mar 2026 16:08:31 +0000 Subject: [PATCH 2/3] Rename winit-example to player Signed-off-by: Nico Burns --- Cargo.lock | 30 +++++++++++++------------- Cargo.toml | 2 +- examples/{winit => player}/Cargo.toml | 2 +- examples/{winit => player}/src/main.rs | 0 4 files changed, 17 insertions(+), 17 deletions(-) rename examples/{winit => player}/Cargo.toml (95%) rename examples/{winit => player}/src/main.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 41d2d97..3bf8507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2615,6 +2615,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "player" +version = "0.1.0" +dependencies = [ + "anyrender", + "anyrender_serialize", + "anyrender_skia", + "anyrender_vello", + "anyrender_vello_cpu", + "anyrender_vello_hybrid", + "kurbo", + "peniko", + "winit", +] + [[package]] name = "png" version = "0.18.1" @@ -4647,21 +4662,6 @@ dependencies = [ "xkbcommon-dl", ] -[[package]] -name = "winit-example" -version = "0.1.0" -dependencies = [ - "anyrender", - "anyrender_serialize", - "anyrender_skia", - "anyrender_vello", - "anyrender_vello_cpu", - "anyrender_vello_hybrid", - "kurbo", - "peniko", - "winit", -] - [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index 47dbd74..5300213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "crates/wgpu_context", "crates/pixels_window_renderer", "crates/softbuffer_window_renderer", - "examples/winit", + "examples/player", "examples/bunnymark", "examples/serialize", ] diff --git a/examples/winit/Cargo.toml b/examples/player/Cargo.toml similarity index 95% rename from examples/winit/Cargo.toml rename to examples/player/Cargo.toml index f02f7d6..0640151 100644 --- a/examples/winit/Cargo.toml +++ b/examples/player/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "winit-example" +name = "player" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/examples/winit/src/main.rs b/examples/player/src/main.rs similarity index 100% rename from examples/winit/src/main.rs rename to examples/player/src/main.rs From 6f8a48288f9f629e950ebfd2d2af97f4488fb755 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 24 Mar 2026 21:11:28 +0000 Subject: [PATCH 3/3] Vendor kurbo SVG path parser (+revert to stable kurbo) Signed-off-by: Nico Burns --- Cargo.lock | 12 +- Cargo.toml | 3 +- crates/anyrender/src/lib.rs | 5 + crates/anyrender/src/recording.rs | 4 +- crates/anyrender/src/svg_path_parser.rs | 356 ++++++++++++++++++++++++ 5 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 crates/anyrender/src/svg_path_parser.rs diff --git a/Cargo.lock b/Cargo.lock index 3bf8507..c8705e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,11 +1786,11 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" version = "0.13.0" -source = "git+https://github.com/linebender/kurbo?rev=868cd631beee6f787d63113a9eb72e8c005fa33c#868cd631beee6f787d63113a9eb72e8c005fa33c" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" dependencies = [ "arrayvec", "euclid", - "polycool", "serde", "smallvec", ] @@ -2669,14 +2669,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" -[[package]] -name = "polycool" -version = "0.4.0" -source = "git+https://github.com/linebender/kurbo?rev=868cd631beee6f787d63113a9eb72e8c005fa33c#868cd631beee6f787d63113a9eb72e8c005fa33c" -dependencies = [ - "arrayvec", -] - [[package]] name = "portable-atomic" version = "1.13.1" diff --git a/Cargo.toml b/Cargo.toml index 5300213..912a66b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,7 @@ pollster = "0.4" # Dev-dependencies winit = { version = "0.30.2", features = ["rwh_06"] } -[patch.crates-io] -kurbo = { git = "https://github.com/linebender/kurbo", rev = "868cd631beee6f787d63113a9eb72e8c005fa33c" } +# [patch.crates-io] # vello = { path = "../vello/vello" } # vello_cpu = { path = "../vello/sparse_strips/vello_cpu" } # vello_hybrid = { path = "../vello/sparse_strips/vello_hybrid" } diff --git a/crates/anyrender/src/lib.rs b/crates/anyrender/src/lib.rs index 1048cb0..f4759c5 100644 --- a/crates/anyrender/src/lib.rs +++ b/crates/anyrender/src/lib.rs @@ -27,6 +27,8 @@ //! - [anyrender_vello](https://docs.rs/anyrender_vello) //! - [anyrender_vello_cpu](https://docs.rs/anyrender_vello_cpu) +#![allow(clippy::collapsible_if)] + use kurbo::{Affine, Rect, Shape, Stroke}; use peniko::{BlendMode, Brush, Color, Fill, FontData, ImageBrushRef, StyleRef}; use recording::RenderCommand; @@ -41,6 +43,9 @@ pub use null_backend::*; pub mod recording; pub use recording::Scene; +#[cfg(feature = "serde")] +mod svg_path_parser; + /// Abstraction for rendering a scene to a window pub trait WindowRenderer { type ScenePainter<'a>: PaintScene diff --git a/crates/anyrender/src/recording.rs b/crates/anyrender/src/recording.rs index 094cc2e..96026ba 100644 --- a/crates/anyrender/src/recording.rs +++ b/crates/anyrender/src/recording.rs @@ -299,6 +299,8 @@ mod svg_path { use kurbo::BezPath; use serde::{self, Deserialize, Deserializer, Serializer}; + use crate::svg_path_parser; + pub fn serialize(path: &BezPath, serializer: S) -> Result where S: Serializer, @@ -311,6 +313,6 @@ mod svg_path { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - BezPath::from_svg(&s).map_err(serde::de::Error::custom) + svg_path_parser::parse_svg_path(&s).map_err(serde::de::Error::custom) } } diff --git a/crates/anyrender/src/svg_path_parser.rs b/crates/anyrender/src/svg_path_parser.rs new file mode 100644 index 0000000..cc736ad --- /dev/null +++ b/crates/anyrender/src/svg_path_parser.rs @@ -0,0 +1,356 @@ +// Copyright 2018 the Kurbo Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A fork of Kurbo's SVG path parser which adds support for NaN and Infinity values + +use core::error::Error; +use core::fmt::{self, Display, Formatter}; + +use peniko::kurbo::{Arc, BezPath, Point, SvgArc}; + +/// Try to parse a bezier path from an SVG path element. +/// +/// 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. +pub(crate) fn parse_svg_path(data: &str) -> Result { + let mut lexer = SvgLexer::new(data); + let mut path = BezPath::new(); + let mut last_cmd = 0; + let mut last_ctrl = None; + let mut first_pt = Point::ORIGIN; + let mut implicit_moveto = None; + while let Some(c) = lexer.get_cmd(last_cmd) { + if c != b'm' && c != b'M' { + if path.elements().is_empty() { + return Err(SvgParseError::UninitializedPath); + } + + if let Some(pt) = implicit_moveto.take() { + path.move_to(pt); + } + } + match c { + b'm' | b'M' => { + implicit_moveto = None; + let pt = lexer.get_maybe_relative(c)?; + path.move_to(pt); + lexer.last_pt = pt; + first_pt = pt; + last_ctrl = Some(pt); + last_cmd = c - (b'M' - b'L'); + } + b'l' | b'L' => { + let pt = lexer.get_maybe_relative(c)?; + path.line_to(pt); + lexer.last_pt = pt; + last_ctrl = Some(pt); + last_cmd = c; + } + b'h' | b'H' => { + let mut x = lexer.get_number()?; + lexer.opt_comma(); + if c == b'h' { + x += lexer.last_pt.x; + } + let pt = Point::new(x, lexer.last_pt.y); + path.line_to(pt); + lexer.last_pt = pt; + last_ctrl = Some(pt); + last_cmd = c; + } + b'v' | b'V' => { + let mut y = lexer.get_number()?; + lexer.opt_comma(); + if c == b'v' { + y += lexer.last_pt.y; + } + let pt = Point::new(lexer.last_pt.x, y); + path.line_to(pt); + lexer.last_pt = pt; + last_ctrl = Some(pt); + last_cmd = c; + } + b'q' | b'Q' => { + let p1 = lexer.get_maybe_relative(c)?; + let p2 = lexer.get_maybe_relative(c)?; + path.quad_to(p1, p2); + last_ctrl = Some(p1); + lexer.last_pt = p2; + last_cmd = c; + } + b't' | b'T' => { + let p1 = match last_ctrl { + Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(), + None => lexer.last_pt, + }; + let p2 = lexer.get_maybe_relative(c)?; + path.quad_to(p1, p2); + last_ctrl = Some(p1); + lexer.last_pt = p2; + last_cmd = c; + } + b'c' | b'C' => { + let p1 = lexer.get_maybe_relative(c)?; + let p2 = lexer.get_maybe_relative(c)?; + let p3 = lexer.get_maybe_relative(c)?; + path.curve_to(p1, p2, p3); + last_ctrl = Some(p2); + lexer.last_pt = p3; + last_cmd = c; + } + b's' | b'S' => { + let p1 = match last_ctrl { + Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(), + None => lexer.last_pt, + }; + let p2 = lexer.get_maybe_relative(c)?; + let p3 = lexer.get_maybe_relative(c)?; + path.curve_to(p1, p2, p3); + last_ctrl = Some(p2); + lexer.last_pt = p3; + last_cmd = c; + } + b'a' | b'A' => { + let radii = lexer.get_number_pair()?; + let x_rotation = lexer.get_number()?.to_radians(); + lexer.opt_comma(); + let large_arc = lexer.get_flag()?; + lexer.opt_comma(); + let sweep = lexer.get_flag()?; + lexer.opt_comma(); + let p = lexer.get_maybe_relative(c)?; + let svg_arc = SvgArc { + from: lexer.last_pt, + to: p, + radii: radii.to_vec2(), + x_rotation, + large_arc, + sweep, + }; + + match Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + // TODO: consider making tolerance configurable + arc.to_cubic_beziers(0.1, |p1, p2, p3| { + path.curve_to(p1, p2, p3); + }); + } + None => { + path.line_to(p); + } + } + + last_ctrl = Some(p); + lexer.last_pt = p; + last_cmd = c; + } + b'z' | b'Z' => { + path.close_path(); + lexer.last_pt = first_pt; + implicit_moveto = Some(first_pt); + } + _ => return Err(SvgParseError::UnknownCommand(c as char)), + } + } + Ok(path) +} + +/// An error which can be returned when parsing an SVG. +#[derive(Debug)] +#[non_exhaustive] +pub(crate) enum SvgParseError { + /// A number was expected. + Wrong, + /// The input string ended while still expecting input. + UnexpectedEof, + /// Encountered an unknown command letter. + UnknownCommand(char), + /// Encountered a command that precedes expected 'moveto' command. + UninitializedPath, +} + +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}\""), + SvgParseError::UninitializedPath => { + write!(f, "Uninitialized path (missing moveto command)") + } + } + } +} + +impl Error for SvgParseError {} + +struct SvgLexer<'a> { + data: &'a str, + ix: usize, + pub last_pt: Point, +} + +impl SvgLexer<'_> { + 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_str(&mut self, s: &[u8]) -> Result<(), SvgParseError> { + for &expected_c in s.iter() { + let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; + if c != expected_c { + return Err(SvgParseError::Wrong); + } + } + + Ok(()) + } + + fn get_number(&mut self) -> Result { + self.skip_ws(); + let start = self.ix; + let mut is_negative = false; + let mut c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; + + // Handle NaN + if c == b'n' || c == b'N' { + self.unget(); + self.get_str(b"NaN")?; + return Ok(f64::NAN); + } + + // If first byte is + or - then read the next byte + if c == b'-' || c == b'+' { + is_negative = c == b'-'; + c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?; + } + + // Handle Infinity, +Infinity, and -Infinity + if c == b'i' || c == b'I' { + self.get_str(b"Infinity")?; + if is_negative { + return Ok(-f64::INFINITY); + } else { + return Ok(f64::INFINITY); + }; + } + + // Reset back by 1 byte after checking for NaN and Infinity + 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(); + } + } + } +}