diff --git a/Cargo.lock b/Cargo.lock index c36f308..3ee8374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "svg2gcode" -version = "0.0.6" +version = "0.0.7" dependencies = [ "cairo-rs", "euclid", diff --git a/cli/src/main.rs b/cli/src/main.rs index 5a9946a..b4bfbec 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,9 +12,7 @@ use std::{ use structopt::StructOpt; use svgtypes::LengthListParser; -use svg2gcode::{ - set_origin, svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality, -}; +use svg2gcode::{svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality}; #[derive(Debug, StructOpt)] #[structopt(name = "svg2gcode", author, about)] @@ -113,7 +111,6 @@ fn main() -> io::Result<()> { } } { - let postprocess = &mut settings.postprocess; if let Some(origin) = opt.origin { for (i, dimension_origin) in origin .split(',') @@ -121,13 +118,13 @@ fn main() -> io::Result<()> { if point.is_empty() { Default::default() } else { - point.parse().expect("could not parse coordinate") + point.parse::().expect("could not parse coordinate") } }) .take(2) .enumerate() { - postprocess.origin[i] = dimension_origin; + settings.conversion.origin[i] = Some(dimension_origin); } } } @@ -135,11 +132,11 @@ fn main() -> io::Result<()> { }; if let Some(export_path) = opt.export { - let mut config_json_bytes = serde_json::to_vec_pretty(&settings)?; + let config_json_bytes = serde_json::to_vec_pretty(&settings)?; if export_path.to_string_lossy() == "-" { - return io::stdout().write_all(&mut config_json_bytes); + return io::stdout().write_all(&config_json_bytes); } else { - return File::create(export_path)?.write_all(&mut config_json_bytes); + return File::create(export_path)?.write_all(&config_json_bytes); } } @@ -253,9 +250,7 @@ fn main() -> io::Result<()> { let document = roxmltree::Document::parse(&input).unwrap(); - let mut program = svg2program(&document, &settings.conversion, options, machine); - - set_origin(&mut program, settings.postprocess.origin); + let program = svg2program(&document, &settings.conversion, options, machine); if let Some(out_path) = opt.out { format_gcode_io(&program, FormatOptions::default(), File::create(out_path)?) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fe17654..79eb719 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "svg2gcode" -version = "0.0.6" +version = "0.0.7" authors = ["Sameer Puri "] edition = "2018" description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine." diff --git a/lib/src/converter/length_serde.rs b/lib/src/converter/length_serde.rs index be9cd31..4bcced8 100644 --- a/lib/src/converter/length_serde.rs +++ b/lib/src/converter/length_serde.rs @@ -11,7 +11,7 @@ where { let mut seq = serializer.serialize_seq(Some(2))?; for i in 0..2 { - let length_def = length[i].clone().map(|length| LengthDef { + let length_def = length[i].map(|length| LengthDef { number: length.number, unit: length.unit, }); diff --git a/lib/src/converter/mod.rs b/lib/src/converter/mod.rs index f339f72..4498106 100644 --- a/lib/src/converter/mod.rs +++ b/lib/src/converter/mod.rs @@ -1,7 +1,7 @@ +use std::fmt::Debug; use std::str::FromStr; -use std::{borrow::Cow, fmt::Debug}; -use g_code::{command, emit::Token}; +use g_code::emit::Token; use log::{debug, warn}; use lyon_geom::{ euclid::{default::Transform2D, Angle, Transform3D}, @@ -35,6 +35,13 @@ pub struct ConversionConfig { pub feedrate: f64, /// Dots per inch for pixels, picas, points, etc. pub dpi: f64, + /// Set the origin point for this conversion + #[cfg_attr(feature = "serde", serde(default = "zero_origin"))] + pub origin: [Option; 2], +} + +const fn zero_origin() -> [Option; 2] { + [Some(0.); 2] } impl Default for ConversionConfig { @@ -43,6 +50,7 @@ impl Default for ConversionConfig { tolerance: 0.002, feedrate: 300.0, dpi: 96.0, + origin: [Some(0.); 2], } } } @@ -70,26 +78,31 @@ struct ConversionVisitor<'a, T: Turtle> { impl<'a, 'input: 'a> ConversionVisitor<'a, GCodeTurtle<'input>> { fn begin(&mut self) { + // Part 1 of converting from SVG to g-code coordinates + self.terrarium.push_transform(Transform2D::scale(1., -1.)); + self.terrarium.turtle.begin(); + } + + fn end(&mut self) { + self.terrarium.pop_transform(); + self.terrarium.turtle.end(); + } +} +impl<'a, 'input: 'a> ConversionVisitor<'a, PreprocessTurtle> { + fn begin(&mut self) { // Part 1 of converting from SVG to g-code coordinates self.terrarium.push_transform(Transform2D::scale(1., -1.)); } fn end(&mut self) { - self.terrarium.pop_all_transforms(); - self.terrarium.turtle.end(); + self.terrarium.pop_transform(); } } impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> { fn visit(&mut self, node: Node) { - // Depth-first SVG DOM traversal - - if node.node_type() != roxmltree::NodeType::Element { - debug!("Encountered a non-element: {:?}", node); - } - if node.tag_name().name() == CLIP_PATH_TAG_NAME { warn!("Clip paths are not supported: {:?}", node); } @@ -198,13 +211,13 @@ impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> { }); comment += &node_name(&node); self.terrarium.turtle.comment(comment); - apply_path(&mut self.terrarium, &self.config, d); + apply_path(&mut self.terrarium, d); } else { warn!("There is a path node containing no actual path: {:?}", node); } } - if node.has_children() { + if node.first_element_child().is_some() { self.name_stack.push(node_name(&node)); } else { // Pop transform since this is the only element that has it @@ -212,13 +225,16 @@ impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> { let mut parent = Some(node); while let Some(p) = parent { - if p.next_sibling().is_some() || p.is_root() { + if p.next_sibling_element().is_some() + || p.is_root() + || p.tag_name().name() == SVG_TAG_NAME + { break; } // Pop the parent transform since this is the last child self.terrarium.pop_transform(); self.name_stack.pop(); - parent = p.parent(); + parent = p.parent_element(); } } } @@ -230,6 +246,32 @@ pub fn svg2program<'a, 'input: 'a>( options: ConversionOptions, machine: Machine<'input>, ) -> Vec> { + let bounding_box = { + let mut visitor = ConversionVisitor { + terrarium: Terrarium::new(PreprocessTurtle::default()), + config, + options: options.clone(), + name_stack: vec![], + }; + + visitor.begin(); + visit::depth_first_visit(doc, &mut visitor); + visitor.end(); + + visitor.terrarium.turtle.bounding_box + }; + + let origin_transform = { + let mut transform = Transform2D::identity(); + if let Some(origin_x) = config.origin[0] { + transform = transform.then_translate(vector(origin_x - bounding_box.min.x, 0.)); + } + if let Some(origin_y) = config.origin[1] { + transform = transform.then_translate(vector(0., origin_y - bounding_box.min.y)); + } + transform + }; + let mut conversion_visitor = ConversionVisitor { terrarium: Terrarium::new(GCodeTurtle { machine, @@ -241,9 +283,13 @@ pub fn svg2program<'a, 'input: 'a>( options, name_stack: vec![], }; + conversion_visitor + .terrarium + .push_transform(origin_transform); conversion_visitor.begin(); visit::depth_first_visit(doc, &mut conversion_visitor); conversion_visitor.end(); + conversion_visitor.terrarium.pop_transform(); conversion_visitor.terrarium.turtle.program } @@ -257,25 +303,21 @@ fn node_name(node: &Node) -> String { name } -fn apply_path<'input, T: Turtle + Debug>( - turtle: &mut Terrarium, - config: &ConversionConfig, - path: &str, -) { +fn apply_path(terrarium: &mut Terrarium, path: &str) { use PathSegment::*; PathParser::from(path) .map(|segment| segment.expect("could not parse path segment")) .for_each(|segment| { debug!("Drawing {:?}", &segment); match segment { - MoveTo { abs, x, y } => turtle.move_to(abs, x, y), + MoveTo { abs, x, y } => terrarium.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() + terrarium.close() } - 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), + LineTo { abs, x, y } => terrarium.line(abs, x, y), + HorizontalLineTo { abs, x } => terrarium.line(abs, x, None), + VerticalLineTo { abs, y } => terrarium.line(abs, None, y), CurveTo { abs, x1, @@ -284,14 +326,16 @@ fn apply_path<'input, T: Turtle + Debug>( y2, x, y, - } => turtle.cubic_bezier(abs, point(x1, y1), point(x2, y2), point(x, y)), + } => terrarium.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)) + terrarium.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)) + terrarium.quadratic_bezier(abs, point(x1, y1), point(x, y)) + } + SmoothQuadratic { abs, x, y } => { + terrarium.smooth_quadratic_bezier(abs, point(x, y)) } - SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier(abs, point(x, y)), EllipticalArc { abs, rx, @@ -301,7 +345,7 @@ fn apply_path<'input, T: Turtle + Debug>( sweep, x, y, - } => turtle.elliptical( + } => terrarium.elliptical( abs, vector(rx, ry), Angle::degrees(x_axis_rotation), diff --git a/lib/src/converter/visit.rs b/lib/src/converter/visit.rs index d9106e8..b5f8ac0 100644 --- a/lib/src/converter/visit.rs +++ b/lib/src/converter/visit.rs @@ -5,9 +5,14 @@ pub trait XmlVisitor { } pub fn depth_first_visit(doc: &Document, visitor: &mut impl XmlVisitor) { - let mut stack = doc.root().children().rev().collect::>(); + let mut stack = doc + .root() + .children() + .rev() + .filter(|x| x.is_element()) + .collect::>(); while let Some(node) = stack.pop() { visitor.visit(node); - stack.extend(node.children().rev()); + stack.extend(node.children().rev().filter(|x| x.is_element())); } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 68a0d12..f0f0415 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -51,8 +51,7 @@ mod test { None, None, ); - let mut program = converter::svg2program(&document, &config, options, machine); - postprocess::set_origin(&mut program, [0., 0.]); + let program = converter::svg2program(&document, &config, options, machine); let mut acc = String::new(); format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap(); @@ -133,4 +132,34 @@ mod test { include_str!("../tests/smooth_curves_circular_interpolation.gcode") ); } + + #[test] + #[cfg(feature = "serde")] + fn deserialize_v1_config_succeeds() { + let json = r#" + { + "conversion": { + "tolerance": 0.002, + "feedrate": 300.0, + "dpi": 96.0 + }, + "machine": { + "supported_functionality": { + "circular_interpolation": true + }, + "tool_on_sequence": null, + "tool_off_sequence": null, + "begin_sequence": null, + "end_sequence": null + }, + "postprocess": { + "origin": [ + 0.0, + 0.0 + ] + } + } + "#; + serde_json::from_str::(json).unwrap(); + } } diff --git a/lib/src/postprocess.rs b/lib/src/postprocess.rs index 418868f..2515951 100644 --- a/lib/src/postprocess.rs +++ b/lib/src/postprocess.rs @@ -13,10 +13,18 @@ type F64Point = Point; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Default, Clone, PartialEq)] pub struct PostprocessConfig { + #[deprecated( + since = "0.7.0", + note = "Setting the origin is now a preprocessing operation" + )] pub origin: [f64; 2], } /// Moves all the commands so that they are beyond a specified position +#[deprecated( + since = "0.7.0", + note = "Setting the origin is now a preprocessing operation" +)] pub fn set_origin(tokens: &mut [Token<'_>], origin: [f64; 2]) { let offset = -get_bounding_box(tokens.iter()).min.to_vector() + F64Point::from(origin).to_vector(); diff --git a/lib/src/turtle/g_code.rs b/lib/src/turtle/g_code.rs index 904ab4a..0d45f4c 100644 --- a/lib/src/turtle/g_code.rs +++ b/lib/src/turtle/g_code.rs @@ -2,12 +2,11 @@ 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 lyon_geom::{CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc}; +use super::Turtle; use crate::arc::{ArcOrLineSegment, FlattenWithArcs}; use crate::machine::Machine; -use super::Turtle; /// Turtle graphics simulator for mapping path segments into g-code #[derive(Debug)] diff --git a/lib/src/turtle/mod.rs b/lib/src/turtle/mod.rs index f800edf..e320b4d 100644 --- a/lib/src/turtle/mod.rs +++ b/lib/src/turtle/mod.rs @@ -1,13 +1,16 @@ 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}; +use lyon_geom::{ + euclid::{default::Transform2D, Angle}, + point, vector, ArcFlags, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc, Vector, +}; -use crate::arc::{Transformed}; +use crate::arc::Transformed; mod g_code; +mod preprocess; pub use self::g_code::GCodeTurtle; +pub use preprocess::PreprocessTurtle; pub trait Turtle: Debug { fn begin(&mut self); @@ -27,7 +30,7 @@ pub struct Terrarium { current_position: Point, initial_position: Point, current_transform: Transform2D, - transform_stack: Vec>, + pub transform_stack: Vec>, previous_quadratic_control: Option>, previous_cubic_control: Option>, } diff --git a/lib/src/turtle/preprocess.rs b/lib/src/turtle/preprocess.rs new file mode 100644 index 0000000..6b8fca8 --- /dev/null +++ b/lib/src/turtle/preprocess.rs @@ -0,0 +1,40 @@ +use lyon_geom::{Box2D, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc}; + +use super::Turtle; + +#[derive(Debug, Default)] +pub struct PreprocessTurtle { + pub bounding_box: Box2D, +} + +impl Turtle for PreprocessTurtle { + fn begin(&mut self) {} + + fn end(&mut self) {} + + fn comment(&mut self, _comment: String) {} + + fn move_to(&mut self, to: Point) { + self.bounding_box = Box2D::from_points([self.bounding_box.min, self.bounding_box.max, to]); + } + + fn line_to(&mut self, to: Point) { + self.bounding_box = Box2D::from_points([self.bounding_box.min, self.bounding_box.max, to]); + } + + fn arc(&mut self, svg_arc: SvgArc) { + if svg_arc.is_straight_line() { + self.line_to(svg_arc.to); + } else { + self.bounding_box = self.bounding_box.union(&svg_arc.to_arc().bounding_box()); + } + } + + fn cubic_bezier(&mut self, cbs: CubicBezierSegment) { + self.bounding_box = self.bounding_box.union(&cbs.bounding_box()); + } + + fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment) { + self.bounding_box = self.bounding_box.union(&qbs.bounding_box()); + } +} diff --git a/web/src/main.rs b/web/src/main.rs index fba6776..002bc17 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -6,7 +6,7 @@ use g_code::{ }; use log::Level; use roxmltree::Document; -use svg2gcode::{set_origin, svg2program, ConversionOptions, Machine, Turtle}; +use svg2gcode::{svg2program, ConversionOptions, Machine}; use yew::prelude::*; use yewdux::prelude::{Dispatch, Dispatcher}; @@ -111,15 +111,13 @@ impl Component for App { ); let document = Document::parse(svg.content.as_str()).unwrap(); - let mut program = svg2program( + let program = svg2program( &document, &app_state.settings.conversion, options, machine, ); - set_origin(&mut program, app_state.settings.postprocess.origin); - let gcode = { let mut acc = String::new(); format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap(); diff --git a/web/src/state.rs b/web/src/state.rs index f5e5b4b..2d79526 100644 --- a/web/src/state.rs +++ b/web/src/state.rs @@ -35,6 +35,7 @@ impl<'a> TryInto for &'a FormState { tolerance: self.tolerance.clone()?, feedrate: self.feedrate.clone()?, dpi: self.dpi.clone()?, + origin: [Some(self.origin[0].clone()?), Some(self.origin[1].clone()?)], }, machine: MachineConfig { supported_functionality: SupportedFunctionality { @@ -62,8 +63,8 @@ impl From<&Settings> for FormState { .supported_functionality .circular_interpolation, origin: [ - Ok(settings.postprocess.origin[0]), - Ok(settings.postprocess.origin[1]), + Ok(settings.conversion.origin[0].unwrap_or(settings.postprocess.origin[0])), + Ok(settings.conversion.origin[1].unwrap_or(settings.postprocess.origin[1])), ], dpi: Ok(settings.conversion.dpi), tool_on_sequence: settings.machine.tool_on_sequence.clone().map(Result::Ok),