diff --git a/src/converter.rs b/src/converter.rs new file mode 100644 index 0000000..1049bc1 --- /dev/null +++ b/src/converter.rs @@ -0,0 +1,254 @@ +use lyon_geom::{euclid, math}; +use svgdom::{AttributeId, AttributeValue, ElementId, ElementType, PathSegment}; + +#[macro_use] +use crate::*; +use crate::gcode::*; +use crate::machine::*; +use crate::turtle::*; + +/// High-level output options +#[derive(Debug)] +pub struct ProgramOptions { + /// Curve interpolation tolerance in millimeters + pub tolerance: f64, + /// Feedrate in millimeters / minute + pub feedrate: f64, + /// Dots per inch for pixels, picas, points, etc. + pub dpi: f64, + pub origin: (f64, f64), +} + +pub fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machine) -> Vec { + let mut turtle = Turtle::new(mach); + + let mut program = vec![ + command!(CommandWord::UnitsMillimeters, {}), + command!(CommandWord::FeedRateUnitsPerMinute, {}), + ]; + program.append(&mut turtle.machine.program_begin()); + program.append(&mut turtle.machine.absolute()); + program.append(&mut turtle.move_to(true, 0.0, 0.0)); + + 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), + }; + + let id = if let svgdom::QName::Id(id) = *node.tag_name() { + id + } else { + 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)), + ); + } + } + // 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(); + } + } + 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(); + } + } + + let is_clip_path = node.ancestors().any(|ancestor| { + if let svgdom::QName::Id(ancestor_id) = *ancestor.tag_name() { + ancestor_id == ElementId::ClipPath + } 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()); + } + } + } + } + + program.append(&mut turtle.machine.tool_off()); + program.append(&mut turtle.machine.absolute()); + program.append(&mut turtle.move_to(true, 0.0, 0.0)); + program.append(&mut turtle.machine.program_end()); + program.push(command!(CommandWord::ProgramEnd, {})); + + program +} + +/// 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::*; + use uom::si::f64::Length; + use uom::si::length::*; + + let length = match l.unit { + Cm => Length::new::(l.num), + Mm => Length::new::(l.num), + In => Length::new::(l.num), + Pc => Length::new::(l.num) * dpi / 96.0, + Pt => Length::new::(l.num) * dpi / 96.0, + Px => Length::new::(l.num * dpi / 96.0), + other => { + warn!( + "Converting from '{:?}' to millimeters is not supported, treating as millimeters", + other + ); + Length::new::(l.num) + } + }; + + length.get::() +} diff --git a/src/gcode/spec.rs b/src/gcode/spec.rs index 8c6debd..c353aea 100644 --- a/src/gcode/spec.rs +++ b/src/gcode/spec.rs @@ -14,24 +14,16 @@ pub struct Word { pub enum Value { Fractional(u32, Option), Float(f64), - String(Box) + String(Box), } impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Fractional(number, Some(fraction)) => { - write!(f, "{}.{}", number, fraction) - }, - Self::Fractional(number, None) => { - write!(f, "{}", number) - }, - Self::Float(float) => { - write!(f, "{}", float) - }, - Self::String(string) => { - write!(f, "{}", string) - } + Self::Fractional(number, Some(fraction)) => write!(f, "{}.{}", number, fraction), + Self::Fractional(number, None) => write!(f, "{}", number), + Self::Float(float) => write!(f, "{}", float), + Self::String(string) => write!(f, "{}", string), } } } diff --git a/src/machine.rs b/src/machine.rs index c71fd97..7c663a8 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,4 +1,3 @@ -#[macro_use] use crate::gcode::*; //// Direction of the machine spindle @@ -73,8 +72,12 @@ impl Machine { } } - pub fn program_begin(&self) -> Vec { self.program_begin_sequence.clone() } - pub fn program_end(&self) -> Vec { self.program_end_sequence.clone() } + pub fn program_begin(&self) -> Vec { + self.program_begin_sequence.clone() + } + pub fn program_end(&self) -> Vec { + self.program_end_sequence.clone() + } /// Output relative distance field if mode was absolute or unknown. pub fn absolute(&mut self) -> Vec { diff --git a/src/main.rs b/src/main.rs index ace0b91..c7049f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,18 +12,17 @@ use std::env; use std::fs::File; use std::io::{self, Read}; -use lyon_geom::{euclid, math}; -use svgdom::{AttributeId, AttributeValue, ElementId, ElementType, PathSegment}; - +/// Converts an SVG to GCode in an internal representation +mod converter; +/// Defines an internal GCode representation #[macro_use] 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; -use gcode::*; -use machine::*; -use turtle::*; - fn main() -> io::Result<()> { if let Err(_) = env::var("RUST_LOG") { env::set_var("RUST_LOG", "svg2gcode=info") @@ -62,7 +61,7 @@ fn main() -> io::Result<()> { } }; - let options = ProgramOptions { + let options = converter::ProgramOptions { tolerance: matches .value_of("tolerance") .map(|x| x.parse().expect("could not parse tolerance")) @@ -81,260 +80,34 @@ fn main() -> io::Result<()> { .map(|coords| coords.map(|point| point.parse().expect("could not parse coordinate"))) .map(|coords| coords.collect::>()) .map(|coords| (coords[0], coords[1])) - .unwrap_or((0.,0.)) + .unwrap_or((0., 0.)), }; - let machine = Machine::new( - matches.value_of("tool_on_sequence").map(parse_gcode).unwrap_or_default(), - matches.value_of("tool_off_sequence").map(parse_gcode).unwrap_or_default(), + let machine = machine::Machine::new( + matches + .value_of("tool_on_sequence") + .map(gcode::parse_gcode) + .unwrap_or_default(), + matches + .value_of("tool_off_sequence") + .map(gcode::parse_gcode) + .unwrap_or_default(), matches .value_of("begin_sequence") - .map(parse_gcode) + .map(gcode::parse_gcode) + .unwrap_or_default(), + matches + .value_of("end_sequence") + .map(gcode::parse_gcode) .unwrap_or_default(), - matches.value_of("end_sequence").map(parse_gcode).unwrap_or_default(), ); let document = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file"); - let program = svg2program(&document, options, machine); + let program = converter::svg2program(&document, options, machine); if let Some(out_path) = matches.value_of("out") { - program2gcode(program, File::create(out_path)?) + gcode::program2gcode(program, File::create(out_path)?) } else { - program2gcode(program, std::io::stdout()) - } -} - -/// High-level output options -#[derive(Debug)] -struct ProgramOptions { - /// Curve interpolation tolerance in millimeters - tolerance: f64, - /// Feedrate in millimeters / minute - feedrate: f64, - /// Dots per inch for pixels, picas, points, etc. - dpi: f64, - origin: (f64, f64) -} - -fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machine) -> Vec { - let mut turtle = Turtle::new(mach); - - let mut program = vec![ - command!(CommandWord::UnitsMillimeters, {}), - command!(CommandWord::FeedRateUnitsPerMinute, {}), - ]; - program.append(&mut turtle.machine.program_begin()); - program.append(&mut turtle.machine.absolute()); - program.append(&mut turtle.move_to(true, 0.0, 0.0)); - - 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), - }; - - let id = if let svgdom::QName::Id(id) = *node.tag_name() { - id - } else { - 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)), - ); - } - } - // 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(); - } - } - 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(); - } - } - - let is_clip_path = node.ancestors().any(|ancestor| { - if let svgdom::QName::Id(ancestor_id) = *ancestor.tag_name() { - ancestor_id == ElementId::ClipPath - } 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()); - } - } - } + gcode::program2gcode(program, std::io::stdout()) } - - program.append(&mut turtle.machine.tool_off()); - program.append(&mut turtle.machine.absolute()); - program.append(&mut turtle.move_to(true, 0.0, 0.0)); - program.append(&mut turtle.machine.program_end()); - program.push(command!(CommandWord::ProgramEnd, {})); - - program -} - -/// 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::*; - use uom::si::f64::Length; - use uom::si::length::*; - - let length = match l.unit { - Cm => Length::new::(l.num), - Mm => Length::new::(l.num), - In => Length::new::(l.num), - Pc => Length::new::(l.num) * dpi / 96.0, - Pt => Length::new::(l.num) * dpi / 96.0, - Px => Length::new::(l.num * dpi / 96.0), - other => { - warn!( - "Converting from '{:?}' to millimeters is not supported, treating as millimeters", - other - ); - Length::new::(l.num) - } - }; - - length.get::() } diff --git a/src/turtle.rs b/src/turtle.rs index 08fd7eb..e405e7b 100644 --- a/src/turtle.rs +++ b/src/turtle.rs @@ -44,11 +44,23 @@ impl Turtle { let original_current_position = inverse_transform.transform_point(self.current_position); let x = x .into() - .map(|x| if abs { x } else { original_current_position.x + x }) + .map(|x| { + if abs { + x + } else { + original_current_position.x + x + } + }) .unwrap_or(original_current_position.x); let y = y .into() - .map(|y| if abs { y } else { original_current_position.y + y }) + .map(|y| { + if abs { + y + } else { + original_current_position.y + y + } + }) .unwrap_or(original_current_position.y); let mut to = point(x, y); @@ -134,11 +146,23 @@ impl Turtle { let original_current_position = inverse_transform.transform_point(self.current_position); let x = x .into() - .map(|x| if abs { x } else { original_current_position.x + x }) + .map(|x| { + if abs { + x + } else { + original_current_position.x + x + } + }) .unwrap_or(original_current_position.x); let y = y .into() - .map(|y| if abs { y } else { original_current_position.y + y }) + .map(|y| { + if abs { + y + } else { + original_current_position.y + y + } + }) .unwrap_or(original_current_position.y); let mut to = point(x, y); @@ -177,12 +201,7 @@ impl Turtle { .flattened(tolerance) .map(|point| { last_point.set(point); - Self::linear_interpolation( - point.x.into(), - point.y.into(), - z.into(), - f.into(), - ) + Self::linear_interpolation(point.x.into(), point.y.into(), z.into(), f.into()) }) .collect(); self.current_position = last_point.get(); @@ -227,7 +246,8 @@ impl Turtle { let mut to = point(x, y); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); - let original_current_position = inverse_transform.transform_point(self.current_position); + let original_current_position = + inverse_transform.transform_point(self.current_position); ctrl1 += original_current_position.to_vector(); ctrl2 += original_current_position.to_vector(); to += original_current_position.to_vector(); @@ -268,7 +288,8 @@ impl Turtle { let mut to = point(x, y); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); - let original_current_position = inverse_transform.transform_point(self.current_position); + let original_current_position = + inverse_transform.transform_point(self.current_position); ctrl2 += original_current_position.to_vector(); to += original_current_position.to_vector(); } @@ -304,7 +325,8 @@ impl Turtle { let mut to = point(x, y); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); - let original_current_position = inverse_transform.transform_point(self.current_position); + let original_current_position = + inverse_transform.transform_point(self.current_position); to += original_current_position.to_vector(); } to = self.current_transform.transform_point(to); @@ -335,7 +357,8 @@ impl Turtle { let mut to = point(x, y); if !abs { let inverse_transform = self.current_transform.inverse().unwrap(); - let original_current_position = inverse_transform.transform_point(self.current_position); + let original_current_position = + inverse_transform.transform_point(self.current_position); to += original_current_position.to_vector(); ctrl += original_current_position.to_vector(); } @@ -456,7 +479,9 @@ impl Turtle { /// Reset the position of the turtle to the origin in the current transform stack pub fn reset(&mut self) { self.current_position = point(0.0, 0.0); - self.current_position = self.current_transform.transform_point(self.current_position); + self.current_position = self + .current_transform + .transform_point(self.current_position); self.previous_control = None; self.initial_position = self.current_position; }