From f049b76235dd1dc16719d3b8d0da3dead4c54251 Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Sat, 20 Nov 2021 12:23:37 -0800 Subject: [PATCH] lib: refactor converter to DFS visitor trait --- lib/src/converter/mod.rs | 155 ++++++++++++++++++++----------------- lib/src/converter/visit.rs | 13 ++++ 2 files changed, 98 insertions(+), 70 deletions(-) create mode 100644 lib/src/converter/visit.rs diff --git a/lib/src/converter/mod.rs b/lib/src/converter/mod.rs index e6677c3..c8d84fd 100644 --- a/lib/src/converter/mod.rs +++ b/lib/src/converter/mod.rs @@ -19,8 +19,11 @@ use crate::turtle::*; #[cfg(feature = "serde")] mod length_serde; +mod visit; const SVG_TAG_NAME: &str = "svg"; +const CLIP_PATH_TAG_NAME: &str = "clipPath"; +const PATH_TAG_NAME: &str = "path"; /// High-level output configuration #[derive(Debug, Clone, PartialEq)] @@ -57,58 +60,48 @@ pub struct ConversionOptions { pub dimensions: [Option; 2], } - -pub fn svg2program<'input>( - doc: &Document, - config: &ConversionConfig, +struct ConversionVisitor<'a, 'input: 'a> { + turtle: &'a mut Turtle<'input>, + name_stack: Vec, + program: Vec>, + config: &'a ConversionConfig, options: ConversionOptions, - turtle: &'input mut Turtle<'input>, -) -> Vec> { - let mut program = command!(UnitsMillimeters {}) - .into_token_vec() - .drain(..) - .collect::>(); - program.extend(turtle.machine.absolute()); - program.extend(turtle.machine.program_begin()); - program.extend(turtle.machine.absolute()); - - // Part 1 of converting from SVG to g-code coordinates - turtle.push_transform(Transform2D::scale(1., -1.)); - - // Depth-first SVG DOM traversal - let mut node_stack = vec![(doc.root(), doc.root().children())]; - let mut name_stack: Vec = vec![]; - - while let Some((parent, mut children)) = node_stack.pop() { - let node: Node = match children.next() { - Some(child) => { - node_stack.push((parent, children)); - child - } - // Last node in this group has been processed - None => { - if parent.has_attribute("viewBox") - || parent.has_attribute("transform") - || parent.has_attribute("width") - || parent.has_attribute("height") - || (parent.has_tag_name(SVG_TAG_NAME) - && options.dimensions.iter().any(Option::is_some)) - { - turtle.pop_transform(); - } - name_stack.pop(); - continue; - } - }; +} + +impl<'a, 'input: 'a> ConversionVisitor<'a, '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()); + + // Part 1 of converting from SVG to g-code coordinates + self.turtle.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()); + } +} + +impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> { + fn visit(&mut self, node: Node) { + // Depth-first SVG DOM traversal if node.node_type() != roxmltree::NodeType::Element { debug!("Encountered a non-element: {:?}", node); - continue; } - if node.tag_name().name() == "clipPath" { + if node.tag_name().name() == CLIP_PATH_TAG_NAME { warn!("Clip paths are not supported: {:?}", node); - continue; } let mut transforms = vec![]; @@ -126,13 +119,13 @@ pub fn svg2program<'input>( .and_then(|mut parser| parser.next()) .transpose() .expect("could not parse width") - .map(|width| length_to_mm(width, config.dpi, scale_w)), + .map(|width| length_to_mm(width, self.config.dpi, scale_w)), node.attribute("height") .map(LengthListParser::from) .and_then(|mut parser| parser.next()) .transpose() .expect("could not parse height") - .map(|height| length_to_mm(height, config.dpi, scale_h)), + .map(|height| length_to_mm(height, self.config.dpi, scale_h)), ); let aspect_ratio = match (view_box, dimensions) { (_, (Some(ref width), Some(ref height))) => *width / *height, @@ -152,8 +145,8 @@ pub fn svg2program<'input>( } let dimensions_override = [ - options.dimensions[0].map(|dim_x| length_to_mm(dim_x, config.dpi, scale_w)), - options.dimensions[1].map(|dim_y| length_to_mm(dim_y, config.dpi, scale_h)), + self.options.dimensions[0].map(|dim_x| length_to_mm(dim_x, self.config.dpi, scale_w)), + self.options.dimensions[1].map(|dim_y| length_to_mm(dim_y, self.config.dpi, scale_h)), ]; match (dimensions_override, dimensions) { @@ -199,48 +192,70 @@ pub fn svg2program<'input>( ) } - if !transforms.is_empty() { - let transform = transforms + self.turtle.push_transform( + transforms .iter() - .fold(Transform2D::identity(), |acc, t| acc.then(t)); - turtle.push_transform(transform); - } + .fold(Transform2D::identity(), |acc, t| acc.then(t)), + ); - if node.tag_name().name() == "path" { + if node.tag_name().name() == PATH_TAG_NAME { if let Some(d) = node.attribute("d") { - turtle.reset(); + self.turtle.reset(); let mut comment = String::new(); - name_stack.iter().for_each(|name| { + self.name_stack.iter().for_each(|name| { comment += name; comment += " > "; }); comment += &node_name(&node); - program.push(Token::Comment { + self.program.push(Token::Comment { is_inline: false, inner: Cow::Owned(comment), }); - program.extend(apply_path(turtle, config, d)); + self.program + .extend(apply_path(&mut self.turtle, &self.config, d)); } else { 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.is_empty() { - // Pop transform early, since this is the only element that has it - turtle.pop_transform(); + self.name_stack.push(node_name(&node)); + } else { + // Pop transform since this is the only element that has it + self.turtle.pop_transform(); + + let mut parent = Some(node); + while let Some(p) = parent { + if p.next_sibling().is_some() || p.is_root() { + break; + } + // Pop the parent transform since this is the last child + self.turtle.pop_transform(); + self.name_stack.pop(); + parent = p.parent(); + } } } +} - turtle.pop_all_transforms(); - program.extend(turtle.machine.tool_off()); - program.extend(turtle.machine.absolute()); - program.extend(turtle.machine.program_end()); - program.append(&mut command!(ProgramEnd {}).into_token_vec()); +pub fn svg2program<'a, 'input: 'a>( + doc: &'a Document, + config: &ConversionConfig, + options: ConversionOptions, + turtle: &'a mut Turtle<'input>, +) -> Vec> { + let mut conversion_visitor = ConversionVisitor { + turtle, + config, + options, + name_stack: vec![], + program: vec![], + }; + conversion_visitor.begin(); + visit::depth_first_visit(doc, &mut conversion_visitor); + conversion_visitor.end(); - program + conversion_visitor.program } fn node_name(node: &Node) -> String { diff --git a/lib/src/converter/visit.rs b/lib/src/converter/visit.rs new file mode 100644 index 0000000..d9106e8 --- /dev/null +++ b/lib/src/converter/visit.rs @@ -0,0 +1,13 @@ +use roxmltree::{Document, Node}; + +pub trait XmlVisitor { + fn visit(&mut self, node: Node); +} + +pub fn depth_first_visit(doc: &Document, visitor: &mut impl XmlVisitor) { + let mut stack = doc.root().children().rev().collect::>(); + while let Some(node) = stack.pop() { + visitor.visit(node); + stack.extend(node.children().rev()); + } +}