diff --git a/Cargo.lock b/Cargo.lock index 3441e45..f525967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,27 +144,27 @@ dependencies = [ [[package]] name = "paste" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "paste-impl 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "paste-impl 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "paste-impl" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "proc-macro-hack" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -206,18 +206,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "roxmltree" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "xmlparser 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "simplecss" -version = "0.2.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "xmlparser 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -225,11 +217,6 @@ name = "siphasher" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "strsim" version = "0.8.0" @@ -243,24 +230,11 @@ dependencies = [ "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "lyon_geom 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", - "paste 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "paste 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "svgdom 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "uom 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "svgdom" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "roxmltree 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "simplecss 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "roxmltree 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "svgtypes 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "xmlwriter 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uom 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -274,7 +248,7 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", @@ -364,12 +338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "xmlparser" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "xmlwriter" -version = "0.1.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] @@ -392,22 +361,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lyon_geom 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9962a2ba81382716b87d7d358493cb71844c1f9165ddad763cd9f4d3f5474df2" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" -"checksum paste 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "678f27e19361472a23717f11d229a7522ef64605baf0715c896a94b8b6b13a06" -"checksum paste-impl 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "149089128a45d8e377677b08873b4bad2a56618f80e4f28a83862ba250994a30" -"checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" +"checksum paste 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3431e8f72b90f8a7af91dec890d9814000cb371258e0ec7370d93e085361f531" +"checksum paste-impl 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "25af5fc872ba284d8d84608bf8a0fa9b5376c96c23f503b007dfd9e34dde5606" +"checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" "checksum proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" "checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" -"checksum roxmltree 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "330d8f80a274bc3cb608908ee345970e7e24b96907f1ad69615a498bec57871c" -"checksum simplecss 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "596554e63596d556a0dbd681416342ca61c75f1a45203201e7e77d3fa2fa9014" +"checksum roxmltree 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d5001f134077069d87f77c8b9452b690df2445f7a43f1c7ca4a1af8dd505789d" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum svgdom 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4ae7d6df72b3a36c0f003f1f8919c96be43d20d7c015f8dcf2f82531a4fc33b" "checksum svgtypes 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" -"checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" +"checksum syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f14a640819f79b72a710c0be059dce779f9339ae046c8bef12c361d56702146f" "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" @@ -420,5 +386,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum xmlparser 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ecec95f00fb0ff019153e64ea520f87d1409769db3e8f4db3ea588638a3e1cee" -"checksum xmlwriter 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +"checksum xmlparser 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ccb4240203dadf40be2de9369e5c6dec1bf427528115b030baca3334c18362d7" diff --git a/Cargo.toml b/Cargo.toml index 2825fd5..4f72777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" description = "Convert paths in SVG files to GCode for a pen plotter or laser engraver" [dependencies] -svgdom = "0" lyon_geom = "0" clap = "2" log = "0" @@ -14,3 +13,5 @@ env_logger = "0" uom = "0" regex = "1" paste = "0" +roxmltree = "0" +svgtypes = "0" diff --git a/src/converter.rs b/src/converter.rs index b6d3549..ad26532 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,5 +1,13 @@ -use lyon_geom::{euclid, math}; -use svgdom::{AttributeId, AttributeValue, ElementId, ElementType, PathSegment}; +use std::str::FromStr; + +use lyon_geom::{ + euclid::{default::Transform2D, Angle, Transform3D}, + math, +}; +use roxmltree::{Document, Node}; +use svgtypes::{ + LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken, ViewBox, +}; #[macro_use] use crate::*; @@ -18,7 +26,7 @@ pub struct ProgramOptions { pub dpi: f64, } -pub fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machine) -> Vec { +pub fn svg2program(doc: &Document, options: ProgramOptions, mach: Machine) -> Vec { let mut turtle = Turtle::new(mach); let mut program = vec![ @@ -29,192 +37,97 @@ pub fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machin program.append(&mut turtle.machine.absolute()); program.append(&mut turtle.move_to(true, 0.0, 0.0)); + // Depth-first SVG DOM traversal + let mut node_stack = vec![(doc.root(), doc.root().children())]; let mut name_stack: Vec = vec![]; - for edge in doc.root().traverse() { - let (node, is_start) = match edge { - svgdom::NodeEdge::Start(node) => (node, true), - svgdom::NodeEdge::End(node) => (node, false), + while let Some((parent, mut children)) = node_stack.pop() { + let node: Node = match children.next() { + Some(child) => { + node_stack.push((parent, children)); + child + } + None => { + if parent.has_attribute("viewBox") + || parent.has_attribute("transform") + || parent.has_attribute("width") + || parent.has_attribute("height") + { + turtle.pop_transform(); + } + name_stack.pop(); + continue; + } }; - let id = if let svgdom::QName::Id(id) = *node.tag_name() { - id - } else { + if node.node_type() != roxmltree::NodeType::Element { + debug!("Encountered a non-element: {:?}", node); continue; - }; + } - let attributes = node.attributes(); - if let (ElementId::Svg, true) = (id, is_start) { - if let Some(&AttributeValue::ViewBox(view_box)) = - attributes.get_value(AttributeId::ViewBox) - { - turtle.stack_scaling( - euclid::Transform2D::create_scale(1. / view_box.w, 1. / view_box.h) - .post_translate(math::vector(view_box.x, view_box.y)), - ); - } - if let (Some(&AttributeValue::Length(width)), Some(&AttributeValue::Length(height))) = ( - attributes.get_value(AttributeId::Width), - attributes.get_value(AttributeId::Height), - ) { - let width_in_mm = length_to_mm(width, options.dpi); - let height_in_mm = length_to_mm(height, options.dpi); - turtle.stack_scaling( - euclid::Transform2D::create_scale(width_in_mm, -height_in_mm) - .post_translate(math::vector(0.0, height_in_mm)), - ); - } + if node.tag_name().name() == "clipPath" { + warn!("Clip paths are not supported: {:?}", node); + continue; } - // Display named elements in GCode comments - if let ElementId::G = id { - if is_start { - name_stack.push(format!("{}#{}", node.tag_name(), node.id().to_string())); - } else { - name_stack.pop(); - } + + let mut transforms = vec![]; + if let Some(view_box) = node.attribute("viewBox") { + let view_box = ViewBox::from_str(view_box).expect("could not parse viewBox"); + transforms.push( + Transform2D::create_scale(1. / view_box.w, 1. / view_box.h) + .post_translate(math::vector(view_box.x, view_box.y)), + ); } - if let Some(&AttributeValue::Transform(ref transform)) = - attributes.get_value(AttributeId::Transform) - { - if is_start { - turtle.push_transform(lyon_geom::euclid::Transform2D::row_major( - transform.a, - transform.b, - transform.c, - transform.d, - transform.e, - transform.f, - )); - } else { - turtle.pop_transform(); - } + + if let Some(transform) = width_and_height_into_transform(&options, &node) { + transforms.push(transform); + } + + if let Some(transform) = node.attribute("transform") { + let parser = TransformListParser::from(transform); + transforms.append( + &mut parser + .map(|token| { + token.expect("could not parse a transform in a list of transforms") + }) + .map(svg_transform_into_euclid_transform) + .collect(), + ) + } + + if transforms.len() != 0 { + let transform = transforms + .iter() + .fold(Transform2D::identity(), |acc, t| acc.post_transform(t)); + turtle.push_transform(transform); } - let is_clip_path = node.ancestors().any(|ancestor| { - if let svgdom::QName::Id(ancestor_id) = *ancestor.tag_name() { - ancestor_id == ElementId::ClipPath + if node.tag_name().name() == "path" { + if let Some(d) = node.attribute("d") { + turtle.reset(); + let mut comment = String::new(); + name_stack.iter().for_each(|name| { + comment += name; + comment += " > "; + }); + comment += &node_name(&node); + program.push(command!(CommandWord::Comment(Box::new(comment)), {})); + program.append(&mut apply_path(&mut turtle, &options, d)); } else { - false - } - }); - - if node.is_graphic() && is_start && !is_clip_path { - match id { - ElementId::Path => { - if let Some(&AttributeValue::Path(ref path)) = - attributes.get_value(AttributeId::D) - { - let prefix: String = - name_stack.iter().fold(String::new(), |mut acc, name| { - acc += name; - acc += " => "; - acc - }); - program.push(command!( - CommandWord::Comment(Box::new(prefix + &node.id())), - {} - )); - turtle.reset(); - for segment in path.iter() { - program.append(&mut match segment { - PathSegment::MoveTo { abs, x, y } => turtle.move_to(*abs, *x, *y), - PathSegment::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(None, options.feedrate) - } - PathSegment::LineTo { abs, x, y } => { - turtle.line(*abs, *x, *y, None, options.feedrate) - } - PathSegment::HorizontalLineTo { abs, x } => { - turtle.line(*abs, *x, None, None, options.feedrate) - } - PathSegment::VerticalLineTo { abs, y } => { - turtle.line(*abs, None, *y, None, options.feedrate) - } - PathSegment::CurveTo { - abs, - x1, - y1, - x2, - y2, - x, - y, - } => turtle.cubic_bezier( - *abs, - *x1, - *y1, - *x2, - *y2, - *x, - *y, - options.tolerance, - None, - options.feedrate, - ), - PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => turtle - .smooth_cubic_bezier( - *abs, - *x2, - *y2, - *x, - *y, - options.tolerance, - None, - options.feedrate, - ), - PathSegment::Quadratic { abs, x1, y1, x, y } => turtle - .quadratic_bezier( - *abs, - *x1, - *y1, - *x, - *y, - options.tolerance, - None, - options.feedrate, - ), - PathSegment::SmoothQuadratic { abs, x, y } => turtle - .smooth_quadratic_bezier( - *abs, - *x, - *y, - options.tolerance, - None, - options.feedrate, - ), - PathSegment::EllipticalArc { - abs, - rx, - ry, - x_axis_rotation, - large_arc, - sweep, - x, - y, - } => turtle.elliptical( - *abs, - *rx, - *ry, - *x_axis_rotation, - *large_arc, - *sweep, - *x, - *y, - None, - options.feedrate, - options.tolerance, - ), - }); - } - } - } - _ => { - warn!("Node <{} id=\"{}\" .../> is not supported", id, node.id()); - } + warn!("There is a path node containing no actual path: {:?}", node); } } + + if node.has_children() { + node_stack.push((node, node.children())); + name_stack.push(node_name(&node)); + } else if transforms.len() != 0 { + // Pop transform early, since this is the only element that has it + turtle.pop_transform(); + } } - // Critical step for actually move the machine back to the origin + + // Critical step for actually moving the machine back to the origin, just in case SVG is malformed turtle.pop_all_transforms(); program.append(&mut turtle.machine.tool_off()); program.append(&mut turtle.machine.absolute()); @@ -225,12 +138,157 @@ pub fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machin program } +fn node_name(node: &Node) -> String { + let mut name = node.tag_name().name().to_string(); + if let Some(id) = node.attribute("id") { + name += "#"; + name += id; + } + name +} + +fn width_and_height_into_transform( + options: &ProgramOptions, + node: &Node, +) -> Option> { + if let (Some(mut width), Some(mut height)) = ( + node.attribute("width").map(LengthListParser::from), + node.attribute("height").map(LengthListParser::from), + ) { + let width = width + .next() + .expect("no width in width property") + .expect("cannot parse width"); + let height = height + .next() + .expect("no height in height property") + .expect("cannot parse height"); + let width_in_mm = length_to_mm(width, options.dpi); + let height_in_mm = length_to_mm(height, options.dpi); + + Some( + Transform2D::create_scale(width_in_mm, -height_in_mm) + .post_translate(math::vector(0f64, height_in_mm)), + ) + } else { + None + } +} + +fn apply_path<'a>(turtle: &mut Turtle, options: &ProgramOptions, path: &'a str) -> Vec { + use PathSegment::*; + PathParser::from(path) + .map(|segment| segment.expect("could not parse path segment")) + .map(|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(None, options.feedrate) + } + LineTo { abs, x, y } => turtle.line(abs, x, y, None, options.feedrate), + HorizontalLineTo { abs, x } => turtle.line(abs, x, None, None, options.feedrate), + VerticalLineTo { abs, y } => turtle.line(abs, None, y, None, options.feedrate), + CurveTo { + abs, + x1, + y1, + x2, + y2, + x, + y, + } => turtle.cubic_bezier( + abs, + x1, + y1, + x2, + y2, + x, + y, + options.tolerance, + None, + options.feedrate, + ), + SmoothCurveTo { abs, x2, y2, x, y } => turtle.smooth_cubic_bezier( + abs, + x2, + y2, + x, + y, + options.tolerance, + None, + options.feedrate, + ), + Quadratic { abs, x1, y1, x, y } => turtle.quadratic_bezier( + abs, + x1, + y1, + x, + y, + options.tolerance, + None, + options.feedrate, + ), + SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier( + abs, + x, + y, + options.tolerance, + None, + options.feedrate, + ), + EllipticalArc { + abs, + rx, + ry, + x_axis_rotation, + large_arc, + sweep, + x, + y, + } => turtle.elliptical( + abs, + rx, + ry, + x_axis_rotation, + large_arc, + sweep, + x, + y, + None, + options.feedrate, + options.tolerance, + ), + } + }) + .flatten() + .collect() +} + +fn svg_transform_into_euclid_transform(svg_transform: TransformListToken) -> Transform2D { + use TransformListToken::*; + match svg_transform { + Matrix { a, b, c, d, e, f } => Transform2D::row_major(a, b, c, d, e, f), + Translate { tx, ty } => Transform2D::create_translation(tx, ty), + Scale { sx, sy } => Transform2D::create_scale(sx, sy), + Rotate { angle } => Transform2D::create_rotation(Angle::degrees(angle)), + SkewX { angle } => { + warn!("Skew X might not be implemented correctly, please check the GCode output."); + Transform3D::create_skew(Angle::degrees(angle), Angle::degrees(0f64)).to_2d() + } + SkewY { angle } => { + warn!("Skew Y might not be implemented correctly, please check the GCode output."); + Transform3D::create_skew(Angle::degrees(0f64), Angle::degrees(angle)).to_2d() + } + } +} + /// Convenience function for converting absolute lengths to millimeters /// Absolute lengths are listed in [CSS 4 §6.2](https://www.w3.org/TR/css-values/#absolute-lengths) /// Relative lengths in [CSS 4 §6.1](https://www.w3.org/TR/css-values/#relative-lengths) are not supported and will cause a panic. /// A default DPI of 96 is used as per [CSS 4 §7.4](https://www.w3.org/TR/css-values/#resolution), which you can adjust with --dpi -fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 { - use svgdom::LengthUnit::*; +fn length_to_mm(l: svgtypes::Length, dpi: f64) -> f64 { + use svgtypes::LengthUnit::*; use uom::si::f64::Length; use uom::si::length::*; diff --git a/src/gcode/spec.rs b/src/gcode/spec.rs index 5c0064e..f083861 100644 --- a/src/gcode/spec.rs +++ b/src/gcode/spec.rs @@ -21,7 +21,7 @@ impl Into for &Value { fn into(self) -> f64 { match self { Value::Float(f) => *f, - _ => panic!("Unwrapping a non-float") + _ => panic!("Unwrapping a non-float"), } } } diff --git a/src/machine.rs b/src/machine.rs index 7c663a8..f82a413 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -8,14 +8,14 @@ pub enum Direction { } /// Whether the tool is active (i.e. cutting) -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Tool { Off, On, } /// The distance mode for movement commands -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Distance { Absolute, Relative, @@ -23,6 +23,7 @@ pub enum Distance { /// Generic machine state simulation, assuming nothing is known about the machine when initialized. /// This is used to reduce output GCode verbosity and run repetitive actions. +#[derive(Debug)] pub struct Machine { tool_state: Option, distance_mode: Option, diff --git a/src/main.rs b/src/main.rs index fc961bc..d60b5d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,7 @@ #[macro_use] extern crate clap; -extern crate env_logger; #[macro_use] extern crate log; -extern crate lyon_geom; -extern crate regex; -extern crate svgdom; -extern crate uom; use std::env; use std::fs::File; @@ -19,12 +14,12 @@ mod converter; mod gcode; /// Emulates the state of an arbitrary machine that can run GCode mod machine; -/// Provides an interface for drawing lines in GCode -/// This concept is referred to as [Turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics). -mod turtle; /// Operations that are easier to implement after GCode is generated, or would /// over-complicate SVG conversion mod postprocess; +/// Provides an interface for drawing lines in GCode +/// This concept is referred to as [Turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics). +mod turtle; fn main() -> io::Result<()> { if let Err(_) = env::var("RUST_LOG") { @@ -98,7 +93,7 @@ fn main() -> io::Result<()> { .unwrap_or_default(), ); - let document = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file"); + let document = roxmltree::Document::parse(&input).expect("Invalid or unsupported SVG file"); let mut program = converter::svg2program(&document, options, machine); diff --git a/src/turtle.rs b/src/turtle.rs index dd2d1e9..ef0eed8 100644 --- a/src/turtle.rs +++ b/src/turtle.rs @@ -6,25 +6,24 @@ use lyon_geom::math::{point, vector, F64Point}; use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc}; /// Turtle graphics simulator for paths that outputs the gcode representation for each operation. -/// Handles trasforms, scaling, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html +/// Handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html +#[derive(Debug)] pub struct Turtle { current_position: F64Point, initial_position: F64Point, current_transform: Transform2D, - scaling: Option>, transform_stack: Vec>, pub machine: Machine, previous_control: Option, } impl Turtle { - /// Create a turtle at the origin with no scaling or transform + /// Create a turtle at the origin with no transform pub fn new(machine: Machine) -> Self { Self { current_position: point(0.0, 0.0), initial_position: point(0.0, 0.0), current_transform: Transform2D::identity(), - scaling: None, transform_stack: vec![], machine, previous_control: None, @@ -439,32 +438,12 @@ impl Turtle { .collect() } - /// Push a new scaling-only transform onto the stack - /// This is useful for handling things like the viewBox - /// https://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute - pub fn stack_scaling(&mut self, scaling: Transform2D) { - self.current_transform = self.current_transform.post_transform(&scaling); - if let Some(ref current_scaling) = self.scaling { - self.scaling = Some(current_scaling.post_transform(&scaling)); - } else { - self.scaling = Some(scaling); - } - } - /// Push a generic transform onto the stack /// Could be any valid CSS transform https://drafts.csswg.org/css-transforms-1/#typedef-transform-function /// https://www.w3.org/TR/SVG/coords.html#InterfaceSVGTransform pub fn push_transform(&mut self, trans: Transform2D) { self.transform_stack.push(self.current_transform); - if let Some(ref scaling) = self.scaling { - self.current_transform = self - .current_transform - .post_transform(&scaling.inverse().unwrap()) - .pre_transform(&trans) - .post_transform(&scaling); - } else { - self.current_transform = self.current_transform.post_transform(&trans); - } + self.current_transform = self.current_transform.post_transform(&trans); } /// Pop a generic transform off the stack, returning to the previous transform state @@ -485,6 +464,7 @@ impl Turtle { } /// Reset the position of the turtle to the origin in the current transform stack + /// Used for starting a new path pub fn reset(&mut self) { self.current_position = point(0.0, 0.0); self.current_position = self