diff --git a/src/converter.rs b/src/converter.rs index 1049bc1..b6d3549 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -16,7 +16,6 @@ pub struct ProgramOptions { 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 { @@ -215,7 +214,8 @@ pub fn svg2program(doc: &svgdom::Document, options: ProgramOptions, mach: Machin } } } - + // Critical step for actually move the machine back to the origin + turtle.pop_all_transforms(); program.append(&mut turtle.machine.tool_off()); program.append(&mut turtle.machine.absolute()); program.append(&mut turtle.move_to(true, 0.0, 0.0)); diff --git a/src/gcode/spec.rs b/src/gcode/spec.rs index c353aea..5c0064e 100644 --- a/src/gcode/spec.rs +++ b/src/gcode/spec.rs @@ -17,6 +17,15 @@ pub enum Value { String(Box), } +impl Into for &Value { + fn into(self) -> f64 { + match self { + Value::Float(f) => *f, + _ => panic!("Unwrapping a non-float") + } + } +} + impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -44,7 +53,7 @@ macro_rules! commands { /// Commands are the operational unit of GCode /// They consist of an identifying word followed by arguments - #[derive(Clone, PartialEq)] + #[derive(Clone, PartialEq, Debug)] pub struct Command { command_word: CommandWord, arguments: Vec @@ -77,6 +86,25 @@ macro_rules! commands { _ => {} } } + + pub fn word<'a>(&'a self) -> &'a CommandWord { + &self.command_word + } + + pub fn get<'a>(&'a self, letter: char) -> Option<&'a Word> { + let letter = letter.to_ascii_uppercase(); + self.arguments.iter().find(|arg| arg.letter == letter) + } + + pub fn set(&mut self, letter: char, value: Value) { + let letter = letter.to_ascii_uppercase(); + for i in 0..self.arguments.len() { + if self.arguments[i].letter == letter { + self.arguments[i].value = value; + break; + } + } + } } impl Into> for Command { @@ -113,7 +141,7 @@ macro_rules! commands { } } - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq, Debug)] pub enum CommandWord { $( $(#[$outer])* diff --git a/src/main.rs b/src/main.rs index c7049f9..fc961bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,9 @@ 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; fn main() -> io::Result<()> { if let Err(_) = env::var("RUST_LOG") { @@ -41,7 +44,7 @@ fn main() -> io::Result<()> { (@arg begin_sequence: --begin +takes_value "Optional GCode begin sequence (i.e. change to a tool)") (@arg end_sequence: --end +takes_value "Optional GCode end sequence, prior to program end (i.e. change to a tool)") (@arg out: --out -o +takes_value "Output file path (overwrites old files), else writes to stdout") - (@arg origin: --origin +takes_value "Set where the bottom left corner of the SVG will be placed (e.g. 0,0)") + (@arg origin: --origin +takes_value "Set where the bottom left corner of the SVG will be placed (default: 0,0)") ) .get_matches(); @@ -74,13 +77,6 @@ fn main() -> io::Result<()> { .value_of("dpi") .map(|x| x.parse().expect("could not parse DPI")) .unwrap_or(96.0), - origin: matches - .value_of("origin") - .map(|coords| coords.split(',')) - .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.)), }; let machine = machine::Machine::new( @@ -104,7 +100,17 @@ fn main() -> io::Result<()> { let document = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file"); - let program = converter::svg2program(&document, options, machine); + let mut program = converter::svg2program(&document, options, machine); + + let origin = matches + .value_of("origin") + .map(|coords| coords.split(',')) + .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.)); + postprocess::set_origin(&mut program, lyon_geom::math::point(origin.0, origin.1)); + if let Some(out_path) = matches.value_of("out") { gcode::program2gcode(program, File::create(out_path)?) } else { diff --git a/src/postprocess.rs b/src/postprocess.rs new file mode 100644 index 0000000..5558e93 --- /dev/null +++ b/src/postprocess.rs @@ -0,0 +1,63 @@ +use crate::gcode::CommandWord::*; +use crate::gcode::*; +use lyon_geom::math::{point, vector, F64Point}; + +pub fn set_origin(commands: &mut [Command], origin: F64Point) { + let offset = get_bounding_box(commands).0.to_vector() + origin.to_vector(); + + let mut is_relative = false; + let mut current_position = point(0f64, 0f64); + + for i in 0..commands.len() { + match &commands[i].word() { + RapidPositioning | LinearInterpolation => { + let x: f64 = (&commands[i].get('X').unwrap().value).into(); + let y: f64 = (&commands[i].get('Y').unwrap().value).into(); + if is_relative { + current_position += vector(x, y); + } else { + current_position = point(x, y); + commands[i].set('X', Value::Float((current_position + offset).x)); + commands[i].set('Y', Value::Float((current_position + offset).y)); + } + }, + AbsoluteDistanceMode => { + is_relative = false; + } + RelativeDistanceMode => { + is_relative = true; + } + _ => {} + } + } +} + +fn get_bounding_box(commands: &[Command]) -> (F64Point, F64Point) { + let (mut minimum, mut maximum) = (point(0f64, 0f64), point(0f64, 0f64)); + let mut is_relative = false; + let mut current_position = point(0f64, 0f64); + for i in 0..commands.len() { + let command = &commands[i]; + match command.word() { + AbsoluteDistanceMode => { + is_relative = false; + } + RelativeDistanceMode => { + is_relative = true; + } + LinearInterpolation | RapidPositioning => { + let x: f64 = (&command.get('x').unwrap().value).into(); + let y: f64 = (&command.get('y').unwrap().value).into(); + if is_relative { + current_position += vector(x, y) + } else { + current_position = point(x, y); + } + minimum = minimum.min(current_position); + maximum = maximum.max(current_position); + } + _ => (), + } + } + (minimum, maximum) +} diff --git a/src/turtle.rs b/src/turtle.rs index e405e7b..dd2d1e9 100644 --- a/src/turtle.rs +++ b/src/turtle.rs @@ -476,6 +476,14 @@ impl Turtle { .expect("popped when no transforms left"); } + /// Remove all transforms, returning to true absolute coordinates + pub fn pop_all_transforms(&mut self) { + while self.transform_stack.len() != 0 { + self.pop_transform(); + } + self.current_transform = Transform2D::identity(); + } + /// 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);