|
|
@ -19,8 +19,11 @@ use crate::turtle::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "serde")]
|
|
|
|
#[cfg(feature = "serde")]
|
|
|
|
mod length_serde;
|
|
|
|
mod length_serde;
|
|
|
|
|
|
|
|
mod visit;
|
|
|
|
|
|
|
|
|
|
|
|
const SVG_TAG_NAME: &str = "svg";
|
|
|
|
const SVG_TAG_NAME: &str = "svg";
|
|
|
|
|
|
|
|
const CLIP_PATH_TAG_NAME: &str = "clipPath";
|
|
|
|
|
|
|
|
const PATH_TAG_NAME: &str = "path";
|
|
|
|
|
|
|
|
|
|
|
|
/// High-level output configuration
|
|
|
|
/// High-level output configuration
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
@ -57,58 +60,48 @@ pub struct ConversionOptions {
|
|
|
|
pub dimensions: [Option<Length>; 2],
|
|
|
|
pub dimensions: [Option<Length>; 2],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct ConversionVisitor<'a, 'input: 'a> {
|
|
|
|
pub fn svg2program<'input>(
|
|
|
|
turtle: &'a mut Turtle<'input>,
|
|
|
|
doc: &Document,
|
|
|
|
name_stack: Vec<String>,
|
|
|
|
config: &ConversionConfig,
|
|
|
|
program: Vec<Token<'input>>,
|
|
|
|
|
|
|
|
config: &'a ConversionConfig,
|
|
|
|
options: ConversionOptions,
|
|
|
|
options: ConversionOptions,
|
|
|
|
turtle: &'input mut Turtle<'input>,
|
|
|
|
}
|
|
|
|
) -> Vec<Token<'input>> {
|
|
|
|
|
|
|
|
let mut program = command!(UnitsMillimeters {})
|
|
|
|
impl<'a, 'input: 'a> ConversionVisitor<'a, 'input> {
|
|
|
|
.into_token_vec()
|
|
|
|
fn begin(&mut self) {
|
|
|
|
.drain(..)
|
|
|
|
self.program = command!(UnitsMillimeters {})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
.into_token_vec()
|
|
|
|
program.extend(turtle.machine.absolute());
|
|
|
|
.drain(..)
|
|
|
|
program.extend(turtle.machine.program_begin());
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
program.extend(turtle.machine.absolute());
|
|
|
|
self.program.extend(self.turtle.machine.absolute());
|
|
|
|
|
|
|
|
self.program.extend(self.turtle.machine.program_begin());
|
|
|
|
// Part 1 of converting from SVG to g-code coordinates
|
|
|
|
self.program.extend(self.turtle.machine.absolute());
|
|
|
|
turtle.push_transform(Transform2D::scale(1., -1.));
|
|
|
|
|
|
|
|
|
|
|
|
// Part 1 of converting from SVG to g-code coordinates
|
|
|
|
// Depth-first SVG DOM traversal
|
|
|
|
self.turtle.push_transform(Transform2D::scale(1., -1.));
|
|
|
|
let mut node_stack = vec![(doc.root(), doc.root().children())];
|
|
|
|
}
|
|
|
|
let mut name_stack: Vec<String> = vec![];
|
|
|
|
|
|
|
|
|
|
|
|
fn end(&mut self) {
|
|
|
|
while let Some((parent, mut children)) = node_stack.pop() {
|
|
|
|
self.turtle.pop_all_transforms();
|
|
|
|
let node: Node = match children.next() {
|
|
|
|
self.program.extend(self.turtle.machine.tool_off());
|
|
|
|
Some(child) => {
|
|
|
|
self.program.extend(self.turtle.machine.absolute());
|
|
|
|
node_stack.push((parent, children));
|
|
|
|
self.program.extend(self.turtle.machine.program_end());
|
|
|
|
child
|
|
|
|
self.program
|
|
|
|
}
|
|
|
|
.append(&mut command!(ProgramEnd {}).into_token_vec());
|
|
|
|
// Last node in this group has been processed
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
}
|
|
|
|
if parent.has_attribute("viewBox")
|
|
|
|
|
|
|
|
|| parent.has_attribute("transform")
|
|
|
|
impl<'a, 'input: 'a> visit::XmlVisitor for ConversionVisitor<'a, 'input> {
|
|
|
|
|| parent.has_attribute("width")
|
|
|
|
fn visit(&mut self, node: Node) {
|
|
|
|
|| parent.has_attribute("height")
|
|
|
|
// Depth-first SVG DOM traversal
|
|
|
|
|| (parent.has_tag_name(SVG_TAG_NAME)
|
|
|
|
|
|
|
|
&& options.dimensions.iter().any(Option::is_some))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
turtle.pop_transform();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
name_stack.pop();
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if node.node_type() != roxmltree::NodeType::Element {
|
|
|
|
if node.node_type() != roxmltree::NodeType::Element {
|
|
|
|
debug!("Encountered a non-element: {:?}", node);
|
|
|
|
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);
|
|
|
|
warn!("Clip paths are not supported: {:?}", node);
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut transforms = vec![];
|
|
|
|
let mut transforms = vec![];
|
|
|
@ -126,13 +119,13 @@ pub fn svg2program<'input>(
|
|
|
|
.and_then(|mut parser| parser.next())
|
|
|
|
.and_then(|mut parser| parser.next())
|
|
|
|
.transpose()
|
|
|
|
.transpose()
|
|
|
|
.expect("could not parse width")
|
|
|
|
.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")
|
|
|
|
node.attribute("height")
|
|
|
|
.map(LengthListParser::from)
|
|
|
|
.map(LengthListParser::from)
|
|
|
|
.and_then(|mut parser| parser.next())
|
|
|
|
.and_then(|mut parser| parser.next())
|
|
|
|
.transpose()
|
|
|
|
.transpose()
|
|
|
|
.expect("could not parse height")
|
|
|
|
.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) {
|
|
|
|
let aspect_ratio = match (view_box, dimensions) {
|
|
|
|
(_, (Some(ref width), Some(ref height))) => *width / *height,
|
|
|
|
(_, (Some(ref width), Some(ref height))) => *width / *height,
|
|
|
@ -152,8 +145,8 @@ pub fn svg2program<'input>(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let dimensions_override = [
|
|
|
|
let dimensions_override = [
|
|
|
|
options.dimensions[0].map(|dim_x| length_to_mm(dim_x, config.dpi, scale_w)),
|
|
|
|
self.options.dimensions[0].map(|dim_x| length_to_mm(dim_x, self.config.dpi, scale_w)),
|
|
|
|
options.dimensions[1].map(|dim_y| length_to_mm(dim_y, config.dpi, scale_h)),
|
|
|
|
self.options.dimensions[1].map(|dim_y| length_to_mm(dim_y, self.config.dpi, scale_h)),
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
match (dimensions_override, dimensions) {
|
|
|
|
match (dimensions_override, dimensions) {
|
|
|
@ -199,48 +192,70 @@ pub fn svg2program<'input>(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !transforms.is_empty() {
|
|
|
|
self.turtle.push_transform(
|
|
|
|
let transform = transforms
|
|
|
|
transforms
|
|
|
|
.iter()
|
|
|
|
.iter()
|
|
|
|
.fold(Transform2D::identity(), |acc, t| acc.then(t));
|
|
|
|
.fold(Transform2D::identity(), |acc, t| acc.then(t)),
|
|
|
|
turtle.push_transform(transform);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if node.tag_name().name() == "path" {
|
|
|
|
if node.tag_name().name() == PATH_TAG_NAME {
|
|
|
|
if let Some(d) = node.attribute("d") {
|
|
|
|
if let Some(d) = node.attribute("d") {
|
|
|
|
turtle.reset();
|
|
|
|
self.turtle.reset();
|
|
|
|
let mut comment = String::new();
|
|
|
|
let mut comment = String::new();
|
|
|
|
name_stack.iter().for_each(|name| {
|
|
|
|
self.name_stack.iter().for_each(|name| {
|
|
|
|
comment += name;
|
|
|
|
comment += name;
|
|
|
|
comment += " > ";
|
|
|
|
comment += " > ";
|
|
|
|
});
|
|
|
|
});
|
|
|
|
comment += &node_name(&node);
|
|
|
|
comment += &node_name(&node);
|
|
|
|
program.push(Token::Comment {
|
|
|
|
self.program.push(Token::Comment {
|
|
|
|
is_inline: false,
|
|
|
|
is_inline: false,
|
|
|
|
inner: Cow::Owned(comment),
|
|
|
|
inner: Cow::Owned(comment),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
program.extend(apply_path(turtle, config, d));
|
|
|
|
self.program
|
|
|
|
|
|
|
|
.extend(apply_path(&mut self.turtle, &self.config, d));
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
warn!("There is a path node containing no actual path: {:?}", node);
|
|
|
|
warn!("There is a path node containing no actual path: {:?}", node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if node.has_children() {
|
|
|
|
if node.has_children() {
|
|
|
|
node_stack.push((node, node.children()));
|
|
|
|
self.name_stack.push(node_name(&node));
|
|
|
|
name_stack.push(node_name(&node));
|
|
|
|
} else {
|
|
|
|
} else if !transforms.is_empty() {
|
|
|
|
// Pop transform since this is the only element that has it
|
|
|
|
// Pop transform early, since this is the only element that has it
|
|
|
|
self.turtle.pop_transform();
|
|
|
|
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();
|
|
|
|
pub fn svg2program<'a, 'input: 'a>(
|
|
|
|
program.extend(turtle.machine.tool_off());
|
|
|
|
doc: &'a Document,
|
|
|
|
program.extend(turtle.machine.absolute());
|
|
|
|
config: &ConversionConfig,
|
|
|
|
program.extend(turtle.machine.program_end());
|
|
|
|
options: ConversionOptions,
|
|
|
|
program.append(&mut command!(ProgramEnd {}).into_token_vec());
|
|
|
|
turtle: &'a mut Turtle<'input>,
|
|
|
|
|
|
|
|
) -> Vec<Token<'input>> {
|
|
|
|
|
|
|
|
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 {
|
|
|
|
fn node_name(node: &Node) -> String {
|
|
|
|