From 433c4c748285c6679a28c0f16b425942a789cd7c Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Sat, 20 Nov 2021 17:29:39 -0800 Subject: [PATCH] refactor turtle to prepare for preprocessing --- cli/src/main.rs | 5 +- lib/src/converter/mod.rs | 117 ++++------- lib/src/lib.rs | 6 +- lib/src/turtle/g_code.rs | 170 +++++++++++++++ lib/src/{turtle.rs => turtle/mod.rs} | 304 +++++++++------------------ web/src/main.rs | 3 +- 6 files changed, 319 insertions(+), 286 deletions(-) create mode 100644 lib/src/turtle/g_code.rs rename lib/src/{turtle.rs => turtle/mod.rs} (55%) diff --git a/cli/src/main.rs b/cli/src/main.rs index f42291e..5a9946a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -13,7 +13,7 @@ use structopt::StructOpt; use svgtypes::LengthListParser; use svg2gcode::{ - set_origin, svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality, Turtle, + set_origin, svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality, }; #[derive(Debug, StructOpt)] @@ -253,8 +253,7 @@ fn main() -> io::Result<()> { let document = roxmltree::Document::parse(&input).unwrap(); - let mut turtle = Turtle::new(machine); - let mut program = svg2program(&document, &settings.conversion, options, &mut turtle); + let mut program = svg2program(&document, &settings.conversion, options, machine); set_origin(&mut program, settings.postprocess.origin); diff --git a/lib/src/converter/mod.rs b/lib/src/converter/mod.rs index c8d84fd..f339f72 100644 --- a/lib/src/converter/mod.rs +++ b/lib/src/converter/mod.rs @@ -1,5 +1,5 @@ -use std::borrow::Cow; use std::str::FromStr; +use std::{borrow::Cow, fmt::Debug}; use g_code::{command, emit::Token}; use log::{debug, warn}; @@ -15,7 +15,7 @@ use svgtypes::{ ViewBox, }; -use crate::turtle::*; +use crate::{turtle::*, Machine}; #[cfg(feature = "serde")] mod length_serde; @@ -60,39 +60,29 @@ pub struct ConversionOptions { pub dimensions: [Option; 2], } -struct ConversionVisitor<'a, 'input: 'a> { - turtle: &'a mut Turtle<'input>, +#[derive(Debug)] +struct ConversionVisitor<'a, T: Turtle> { + terrarium: Terrarium, name_stack: Vec, - program: Vec>, config: &'a ConversionConfig, options: ConversionOptions, } -impl<'a, 'input: 'a> ConversionVisitor<'a, 'input> { +impl<'a, 'input: 'a> ConversionVisitor<'a, GCodeTurtle<'input>> { fn begin(&mut self) { - self.program = command!(UnitsMillimeters {}) - .into_token_vec() - .drain(..) - .collect::>(); - self.program.extend(self.turtle.machine.absolute()); - self.program.extend(self.turtle.machine.program_begin()); - self.program.extend(self.turtle.machine.absolute()); + self.terrarium.turtle.begin(); // Part 1 of converting from SVG to g-code coordinates - self.turtle.push_transform(Transform2D::scale(1., -1.)); + self.terrarium.push_transform(Transform2D::scale(1., -1.)); } fn end(&mut self) { - self.turtle.pop_all_transforms(); - self.program.extend(self.turtle.machine.tool_off()); - self.program.extend(self.turtle.machine.absolute()); - self.program.extend(self.turtle.machine.program_end()); - self.program - .append(&mut command!(ProgramEnd {}).into_token_vec()); + self.terrarium.pop_all_transforms(); + self.terrarium.turtle.end(); } } -impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { +impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> { fn visit(&mut self, node: Node) { // Depth-first SVG DOM traversal @@ -192,7 +182,7 @@ impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { ) } - self.turtle.push_transform( + self.terrarium.push_transform( transforms .iter() .fold(Transform2D::identity(), |acc, t| acc.then(t)), @@ -200,19 +190,15 @@ impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { if node.tag_name().name() == PATH_TAG_NAME { if let Some(d) = node.attribute("d") { - self.turtle.reset(); + self.terrarium.reset(); let mut comment = String::new(); self.name_stack.iter().for_each(|name| { comment += name; comment += " > "; }); comment += &node_name(&node); - self.program.push(Token::Comment { - is_inline: false, - inner: Cow::Owned(comment), - }); - self.program - .extend(apply_path(&mut self.turtle, &self.config, d)); + self.terrarium.turtle.comment(comment); + apply_path(&mut self.terrarium, &self.config, d); } else { warn!("There is a path node containing no actual path: {:?}", node); } @@ -222,7 +208,7 @@ impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { self.name_stack.push(node_name(&node)); } else { // Pop transform since this is the only element that has it - self.turtle.pop_transform(); + self.terrarium.pop_transform(); let mut parent = Some(node); while let Some(p) = parent { @@ -230,7 +216,7 @@ impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { break; } // Pop the parent transform since this is the last child - self.turtle.pop_transform(); + self.terrarium.pop_transform(); self.name_stack.pop(); parent = p.parent(); } @@ -242,20 +228,24 @@ pub fn svg2program<'a, 'input: 'a>( doc: &'a Document, config: &ConversionConfig, options: ConversionOptions, - turtle: &'a mut Turtle<'input>, + machine: Machine<'input>, ) -> Vec> { let mut conversion_visitor = ConversionVisitor { - turtle, + terrarium: Terrarium::new(GCodeTurtle { + machine, + tolerance: config.tolerance, + feedrate: config.feedrate, + program: vec![], + }), config, options, name_stack: vec![], - program: vec![], }; conversion_visitor.begin(); visit::depth_first_visit(doc, &mut conversion_visitor); conversion_visitor.end(); - conversion_visitor.program + conversion_visitor.terrarium.turtle.program } fn node_name(node: &Node) -> String { @@ -267,25 +257,25 @@ fn node_name(node: &Node) -> String { name } -fn apply_path<'input>( - turtle: &'_ mut Turtle<'input>, +fn apply_path<'input, T: Turtle + Debug>( + turtle: &mut Terrarium, config: &ConversionConfig, path: &str, -) -> Vec> { +) { use PathSegment::*; PathParser::from(path) .map(|segment| segment.expect("could not parse path segment")) - .flat_map(|segment| { + .for_each(|segment| { debug!("Drawing {:?}", &segment); match segment { MoveTo { abs, x, y } => turtle.move_to(abs, x, y), ClosePath { abs: _ } => { // Ignore abs, should have identical effect: [9.3.4. The "closepath" command]("https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand) - turtle.close(config.feedrate) + turtle.close() } - LineTo { abs, x, y } => turtle.line(abs, x, y, config.feedrate), - HorizontalLineTo { abs, x } => turtle.line(abs, x, None, config.feedrate), - VerticalLineTo { abs, y } => turtle.line(abs, None, y, config.feedrate), + LineTo { abs, x, y } => turtle.line(abs, x, y), + HorizontalLineTo { abs, x } => turtle.line(abs, x, None), + VerticalLineTo { abs, y } => turtle.line(abs, None, y), CurveTo { abs, x1, @@ -294,34 +284,14 @@ fn apply_path<'input>( y2, x, y, - } => turtle.cubic_bezier( - abs, - point(x1, y1), - point(x2, y2), - point(x, y), - config.tolerance, - config.feedrate, - ), - SmoothCurveTo { abs, x2, y2, x, y } => turtle.smooth_cubic_bezier( - abs, - point(x2, y2), - point(x, y), - config.tolerance, - config.feedrate, - ), - Quadratic { abs, x1, y1, x, y } => turtle.quadratic_bezier( - abs, - point(x1, y1), - point(x, y), - config.tolerance, - config.feedrate, - ), - SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier( - abs, - point(x, y), - config.tolerance, - config.feedrate, - ), + } => turtle.cubic_bezier(abs, point(x1, y1), point(x2, y2), point(x, y)), + SmoothCurveTo { abs, x2, y2, x, y } => { + turtle.smooth_cubic_bezier(abs, point(x2, y2), point(x, y)) + } + Quadratic { abs, x1, y1, x, y } => { + turtle.quadratic_bezier(abs, point(x1, y1), point(x, y)) + } + SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier(abs, point(x, y)), EllipticalArc { abs, rx, @@ -337,12 +307,9 @@ fn apply_path<'input>( Angle::degrees(x_axis_rotation), ArcFlags { large_arc, sweep }, point(x, y), - config.feedrate, - config.tolerance, ), } - }) - .collect() + }); } fn svg_transform_into_euclid_transform(svg_transform: TransformListToken) -> Transform2D { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 780b4b4..68a0d12 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -42,7 +42,7 @@ mod test { let options = ConversionOptions { dimensions }; let document = roxmltree::Document::parse(input).unwrap(); - let mut turtle = Turtle::new(Machine::new( + let machine = Machine::new( SupportedFunctionality { circular_interpolation, }, @@ -50,8 +50,8 @@ mod test { None, None, None, - )); - let mut program = converter::svg2program(&document, &config, options, &mut turtle); + ); + let mut program = converter::svg2program(&document, &config, options, machine); postprocess::set_origin(&mut program, [0., 0.]); let mut acc = String::new(); diff --git a/lib/src/turtle/g_code.rs b/lib/src/turtle/g_code.rs new file mode 100644 index 0000000..904ab4a --- /dev/null +++ b/lib/src/turtle/g_code.rs @@ -0,0 +1,170 @@ +use std::borrow::Cow; +use std::fmt::Debug; + +use ::g_code::{command, emit::Token}; +use lyon_geom::Point; +use lyon_geom::{CubicBezierSegment, QuadraticBezierSegment, SvgArc}; + +use crate::arc::{ArcOrLineSegment, FlattenWithArcs}; +use crate::machine::Machine; +use super::Turtle; + +/// Turtle graphics simulator for mapping path segments into g-code +#[derive(Debug)] +pub struct GCodeTurtle<'input> { + pub machine: Machine<'input>, + pub tolerance: f64, + pub feedrate: f64, + pub program: Vec>, +} + +impl<'input> GCodeTurtle<'input> { + fn circular_interpolation(&self, svg_arc: SvgArc) -> Vec> { + debug_assert!((svg_arc.radii.x.abs() - svg_arc.radii.y.abs()).abs() < f64::EPSILON); + match (svg_arc.flags.large_arc, svg_arc.flags.sweep) { + (false, true) => command!(CounterclockwiseCircularInterpolation { + X: svg_arc.to.x, + Y: svg_arc.to.y, + R: svg_arc.radii.x, + F: self.feedrate, + }) + .into_token_vec(), + (false, false) => command!(ClockwiseCircularInterpolation { + X: svg_arc.to.x, + Y: svg_arc.to.y, + R: svg_arc.radii.x, + F: self.feedrate, + }) + .into_token_vec(), + (true, _) => { + let (left, right) = svg_arc.to_arc().split(0.5); + let mut token_vec = self.circular_interpolation(left.to_svg_arc()); + token_vec.append(&mut self.circular_interpolation(right.to_svg_arc())); + token_vec + } + } + } + + fn tool_on(&mut self) { + self.program.extend( + self.machine + .tool_on() + .drain(..) + .chain(self.machine.absolute()), + ); + } + + fn tool_off(&mut self) { + self.program.extend( + self.machine + .tool_off() + .drain(..) + .chain(self.machine.absolute()), + ); + } +} + +impl<'input> Turtle for GCodeTurtle<'input> { + fn begin(&mut self) { + self.program + .append(&mut command!(UnitsMillimeters {}).into_token_vec()); + self.program.extend(self.machine.absolute()); + self.program.extend(self.machine.program_begin()); + self.program.extend(self.machine.absolute()); + } + + fn end(&mut self) { + self.program.extend(self.machine.tool_off()); + self.program.extend(self.machine.absolute()); + self.program.extend(self.machine.program_end()); + self.program + .append(&mut command!(ProgramEnd {}).into_token_vec()); + } + + fn comment(&mut self, comment: String) { + self.program.push(Token::Comment { + is_inline: false, + inner: Cow::Owned(comment), + }); + } + + fn move_to(&mut self, to: Point) { + self.tool_off(); + self.program.append( + &mut command!(RapidPositioning { + X: to.x as f64, + Y: to.y as f64, + }) + .into_token_vec(), + ); + } + + fn line_to(&mut self, to: Point) { + self.tool_on(); + self.program.append( + &mut command!(LinearInterpolation { + X: to.x, + Y: to.y, + F: self.feedrate, + }) + .into_token_vec(), + ); + } + + fn arc(&mut self, svg_arc: SvgArc) { + if svg_arc.is_straight_line() { + self.line_to(svg_arc.to); + return; + } + + self.tool_on(); + + if self + .machine + .supported_functionality() + .circular_interpolation + { + FlattenWithArcs::flattened(&svg_arc, self.tolerance) + .drain(..) + .for_each(|segment| match segment { + ArcOrLineSegment::Arc(arc) => { + self.program.append(&mut self.circular_interpolation(arc)) + } + ArcOrLineSegment::Line(line) => { + self.line_to(line.to); + } + }); + } else { + svg_arc + .to_arc() + .flattened(self.tolerance) + .for_each(|point| self.line_to(point)); + }; + } + + fn cubic_bezier(&mut self, cbs: CubicBezierSegment) { + self.tool_on(); + + if self + .machine + .supported_functionality() + .circular_interpolation + { + FlattenWithArcs::::flattened(&cbs, self.tolerance) + .drain(..) + .for_each(|segment| match segment { + ArcOrLineSegment::Arc(arc) => { + self.program.append(&mut self.circular_interpolation(arc)) + } + ArcOrLineSegment::Line(line) => self.line_to(line.to), + }); + } else { + cbs.flattened(self.tolerance) + .for_each(|point| self.line_to(point)); + }; + } + + fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment) { + self.cubic_bezier(qbs.to_cubic()); + } +} diff --git a/lib/src/turtle.rs b/lib/src/turtle/mod.rs similarity index 55% rename from lib/src/turtle.rs rename to lib/src/turtle/mod.rs index 6bfc594..f800edf 100644 --- a/lib/src/turtle.rs +++ b/lib/src/turtle/mod.rs @@ -1,40 +1,54 @@ -use crate::arc::{ArcOrLineSegment, FlattenWithArcs, Transformed}; -use crate::machine::Machine; -use g_code::{command, emit::Token}; +use std::fmt::Debug; + use lyon_geom::euclid::{default::Transform2D, Angle}; use lyon_geom::{point, vector, Point, Vector}; use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc}; -type F64Point = Point; +use crate::arc::{Transformed}; + +mod g_code; +pub use self::g_code::GCodeTurtle; -/// Turtle graphics simulator for paths that outputs the g-code representation for each operation. -/// Handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html +pub trait Turtle: Debug { + fn begin(&mut self); + fn end(&mut self); + fn comment(&mut self, comment: String); + fn move_to(&mut self, to: Point); + fn line_to(&mut self, to: Point); + fn arc(&mut self, svg_arc: SvgArc); + fn cubic_bezier(&mut self, cbs: CubicBezierSegment); + fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment); +} + +/// Wrapper for [Turtle] that handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html #[derive(Debug)] -pub struct Turtle<'input> { - current_position: F64Point, - initial_position: F64Point, +pub struct Terrarium { + pub turtle: T, + current_position: Point, + initial_position: Point, current_transform: Transform2D, transform_stack: Vec>, - pub machine: Machine<'input>, - previous_control: Option, + previous_quadratic_control: Option>, + previous_cubic_control: Option>, } -impl<'input> Turtle<'input> { +impl Terrarium { /// Create a turtle at the origin with no transform - pub fn new(machine: Machine<'input>) -> Self { + pub fn new(turtle: T) -> Self { Self { + turtle, current_position: Point::zero(), initial_position: Point::zero(), current_transform: Transform2D::identity(), transform_stack: vec![], - machine, - previous_control: None, + previous_quadratic_control: None, + previous_cubic_control: None, } } /// Move the turtle to the given absolute/relative coordinates in the current transform /// https://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands - pub fn move_to(&mut self, abs: bool, x: X, y: Y) -> Vec> + pub fn move_to(&mut self, abs: bool, x: X, y: Y) where X: Into>, Y: Into>, @@ -65,89 +79,31 @@ impl<'input> Turtle<'input> { let to = self.current_transform.transform_point(point(x, y)); self.current_position = to; self.initial_position = to; - self.previous_control = None; - - self.machine - .tool_off() - .drain(..) - .chain(self.machine.absolute()) - .chain( - command!(RapidPositioning { - X: to.x as f64, - Y: to.y as f64, - }) - .into_token_vec(), - ) - .collect() - } - - fn linear_interpolation(x: f64, y: f64, feedrate: f64) -> Vec> { - command!(LinearInterpolation { - X: x, - Y: y, - F: feedrate, - }) - .into_token_vec() - } - - fn circular_interpolation(svg_arc: SvgArc, feedrate: f64) -> Vec> { - debug_assert!((svg_arc.radii.x.abs() - svg_arc.radii.y.abs()).abs() < f64::EPSILON); - match (svg_arc.flags.large_arc, svg_arc.flags.sweep) { - (false, true) => command!(CounterclockwiseCircularInterpolation { - X: svg_arc.to.x, - Y: svg_arc.to.y, - R: svg_arc.radii.x, - F: feedrate, - }) - .into_token_vec(), - (false, false) => command!(ClockwiseCircularInterpolation { - X: svg_arc.to.x, - Y: svg_arc.to.y, - R: svg_arc.radii.x, - F: feedrate, - }) - .into_token_vec(), - (true, _) => { - let (left, right) = svg_arc.to_arc().split(0.5); - let mut token_vec = Self::circular_interpolation(left.to_svg_arc(), feedrate); - token_vec.append(&mut Self::circular_interpolation( - right.to_svg_arc(), - feedrate, - )); - token_vec - } - } + self.previous_quadratic_control = None; + self.previous_cubic_control = None; + self.turtle.move_to(to); } /// Close an SVG path, cutting back to its initial position /// https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand - pub fn close(&mut self, feedrate: f64) -> Vec> { + pub fn close(&mut self) { // See https://www.w3.org/TR/SVG/paths.html#Segment-CompletingClosePath // which could result in a G91 G1 X0 Y0 - if (self.current_position - self.initial_position) + if !(self.current_position - self.initial_position) .abs() .lower_than(vector(std::f64::EPSILON, std::f64::EPSILON)) .all() { - return vec![]; + self.turtle.line_to(self.initial_position); } self.current_position = self.initial_position; - - self.machine - .tool_on() - .drain(..) - .chain(self.machine.absolute()) - .chain(Self::linear_interpolation( - self.initial_position.x, - self.initial_position.y, - feedrate, - )) - .collect() + self.previous_quadratic_control = None; + self.previous_cubic_control = None; } /// Draw a line from the current position in the current transform to the specified position /// https://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands - pub fn line(&mut self, abs: bool, x: X, y: Y, feedrate: f64) -> Vec> + pub fn line(&mut self, abs: bool, x: X, y: Y) where X: Into>, Y: Into>, @@ -177,59 +133,10 @@ impl<'input> Turtle<'input> { let to = self.current_transform.transform_point(point(x, y)); self.current_position = to; - self.previous_control = None; - - self.machine - .tool_on() - .drain(..) - .chain(self.machine.absolute()) - .chain(Self::linear_interpolation(to.x, to.y, feedrate)) - .collect() - } + self.previous_quadratic_control = None; + self.previous_cubic_control = None; - /// Draw a cubic bezier curve segment - /// The public bezier functions call this command after converting to a cubic bezier segment - /// https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands - fn bezier( - &mut self, - cbs: CubicBezierSegment, - tolerance: f64, - feedrate: f64, - ) -> Vec> { - let tokens: Vec<_> = if self - .machine - .supported_functionality() - .circular_interpolation - { - FlattenWithArcs::::flattened(&cbs, tolerance) - .drain(..) - .flat_map(|segment| match segment { - ArcOrLineSegment::Arc(arc) => Self::circular_interpolation(arc, feedrate), - ArcOrLineSegment::Line(line) => { - Self::linear_interpolation(line.to.x, line.to.y, feedrate) - } - }) - .collect() - } else { - cbs.flattened(tolerance) - .flat_map(|point| Self::linear_interpolation(point.x, point.y, feedrate)) - .collect() - }; - - self.current_position = cbs.to; - - // See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints - self.previous_control = Some(point( - 2. * self.current_position.x - cbs.ctrl2.x, - 2. * self.current_position.y - cbs.ctrl2.y, - )); - - self.machine - .tool_on() - .drain(..) - .chain(self.machine.absolute()) - .chain(tokens) - .collect() + self.turtle.line_to(to); } /// Draw a cubic curve from the current point to (x, y) with specified control points (x1, y1) and (x2, y2) @@ -240,9 +147,7 @@ impl<'input> Turtle<'input> { mut ctrl1: Point, mut ctrl2: Point, mut to: Point, - tolerance: f64, - feedrate: f64, - ) -> Vec> { + ) { let from = self.current_position; if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); @@ -261,21 +166,24 @@ impl<'input> Turtle<'input> { ctrl2, to, }; - self.bezier(cbs, tolerance, feedrate) + + self.current_position = cbs.to; + + // See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints + self.previous_cubic_control = Some(point( + 2. * self.current_position.x - cbs.ctrl2.x, + 2. * self.current_position.y - cbs.ctrl2.y, + )); + self.previous_quadratic_control = None; + + self.turtle.cubic_bezier(cbs); } /// Draw a shorthand/smooth cubic bezier segment, where the first control point was already given /// https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands - pub fn smooth_cubic_bezier( - &mut self, - abs: bool, - mut ctrl2: Point, - mut to: Point, - tolerance: f64, - feedrate: f64, - ) -> Vec> { + pub fn smooth_cubic_bezier(&mut self, abs: bool, mut ctrl2: Point, mut to: Point) { let from = self.current_position; - let ctrl1 = self.previous_control.unwrap_or(self.current_position); + let ctrl1 = self.previous_cubic_control.unwrap_or(self.current_position); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); let original_current_position = inverse_transform.transform_point(from); @@ -291,20 +199,26 @@ impl<'input> Turtle<'input> { ctrl2, to, }; - self.bezier(cbs, tolerance, feedrate) + + self.current_position = cbs.to; + + // See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints + self.previous_cubic_control = Some(point( + 2. * self.current_position.x - cbs.ctrl2.x, + 2. * self.current_position.y - cbs.ctrl2.y, + )); + self.previous_quadratic_control = None; + + self.turtle.cubic_bezier(cbs); } /// Draw a shorthand/smooth cubic bezier segment, where the control point was already given /// https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands - pub fn smooth_quadratic_bezier( - &mut self, - abs: bool, - mut to: Point, - tolerance: f64, - feedrate: f64, - ) -> Vec> { + pub fn smooth_quadratic_bezier(&mut self, abs: bool, mut to: Point) { let from = self.current_position; - let ctrl = self.previous_control.unwrap_or(self.current_position); + let ctrl = self + .previous_quadratic_control + .unwrap_or(self.current_position); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); let original_current_position = inverse_transform.transform_point(from); @@ -313,19 +227,22 @@ impl<'input> Turtle<'input> { to = self.current_transform.transform_point(to); let qbs = QuadraticBezierSegment { from, ctrl, to }; - self.bezier(qbs.to_cubic(), tolerance, feedrate) + + self.current_position = qbs.to; + + // See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints + self.previous_quadratic_control = Some(point( + 2. * self.current_position.x - qbs.ctrl.x, + 2. * self.current_position.y - qbs.ctrl.y, + )); + self.previous_cubic_control = None; + + self.turtle.quadratic_bezier(qbs); } /// Draw a quadratic bezier segment /// https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands - pub fn quadratic_bezier( - &mut self, - abs: bool, - mut ctrl: Point, - mut to: Point, - tolerance: f64, - feedrate: f64, - ) -> Vec> { + pub fn quadratic_bezier(&mut self, abs: bool, mut ctrl: Point, mut to: Point) { let from = self.current_position; if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); @@ -337,7 +254,17 @@ impl<'input> Turtle<'input> { to = self.current_transform.transform_point(to); let qbs = QuadraticBezierSegment { from, ctrl, to }; - self.bezier(qbs.to_cubic(), tolerance, feedrate) + + self.current_position = qbs.to; + + // See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints + self.previous_quadratic_control = Some(point( + 2. * self.current_position.x - qbs.ctrl.x, + 2. * self.current_position.y - qbs.ctrl.y, + )); + self.previous_cubic_control = None; + + self.turtle.quadratic_bezier(qbs); } /// Draw an elliptical arc segment @@ -349,9 +276,7 @@ impl<'input> Turtle<'input> { x_rotation: Angle, flags: ArcFlags, mut to: Point, - feedrate: f64, - tolerance: f64, - ) -> Vec> { + ) { let from = self .current_transform .inverse() @@ -370,39 +295,11 @@ impl<'input> Turtle<'input> { } .transformed(&self.current_transform); - let arc_tokens = if svg_arc.is_straight_line() { - Self::linear_interpolation(svg_arc.to.x, svg_arc.to.y, feedrate) - } else if self - .machine - .supported_functionality() - .circular_interpolation - { - FlattenWithArcs::flattened(&svg_arc, tolerance) - .drain(..) - .flat_map(|segment| match segment { - ArcOrLineSegment::Arc(arc) => Self::circular_interpolation(arc, feedrate), - ArcOrLineSegment::Line(line) => { - Self::linear_interpolation(line.to.x, line.to.y, feedrate) - } - }) - .collect() - } else { - svg_arc - .to_arc() - .flattened(tolerance) - .flat_map(|point| Self::linear_interpolation(point.x, point.y, feedrate)) - .collect() - }; - self.current_position = svg_arc.to; - self.previous_control = None; - - self.machine - .tool_on() - .drain(..) - .chain(self.machine.absolute()) - .chain(arc_tokens) - .collect() + self.previous_quadratic_control = None; + self.previous_cubic_control = None; + + self.turtle.arc(svg_arc); } /// Push a generic transform onto the stack @@ -435,7 +332,8 @@ impl<'input> Turtle<'input> { self.current_position = self .current_transform .transform_point(self.current_position); - self.previous_control = None; + self.previous_quadratic_control = None; + self.previous_cubic_control = None; self.initial_position = self.current_position; } } diff --git a/web/src/main.rs b/web/src/main.rs index 6ee6cea..fba6776 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -111,12 +111,11 @@ impl Component for App { ); let document = Document::parse(svg.content.as_str()).unwrap(); - let mut turtle = Turtle::new(machine); let mut program = svg2program( &document, &app_state.settings.conversion, options, - &mut turtle, + machine, ); set_origin(&mut program, app_state.settings.postprocess.origin);