Continue documentation efforts & cleanup

master
Sameer Puri 5 years ago
parent f7e037cfdd
commit 5842b2df0b

@ -3,7 +3,7 @@ name = "svg2gcode"
version = "0.0.1"
authors = ["Sameer Puri <purisame@spuri.io>"]
edition = "2018"
description = "Convert paths in SVG files to GCode for a pen plotter"
description = "Convert paths in SVG files to GCode for a pen plotter or laser engraver"
[dependencies]
svgdom = "0"

@ -25,7 +25,7 @@ Convert any SVG 1.1 path to gcode for a pen plotter, laser engraver, etc.
### Input
```bash
cargo run --release -- examples/Vanderbilt_Commodores_logo.svg
cargo run -- examples/Vanderbilt_Commodores_logo.svg --off 'M4' --on 'M5' -o out.gcode
```
![Vanderbilt Commodores Logo](examples/Vanderbilt_Commodores_logo.svg)

@ -0,0 +1,104 @@
use core::convert::TryFrom;
use std::io::{self, Write};
#[macro_use]
mod spec;
pub use spec::*;
/// Collapses GCode words into higher-level commands
pub struct CommandVecIntoIterator {
vec: Vec<Word>,
index: usize,
}
impl Iterator for CommandVecIntoIterator {
type Item = Command;
fn next(&mut self) -> Option<Self::Item> {
if self.vec.len() <= self.index {
return None;
}
let mut i = self.index + 1;
while i < self.vec.len() {
if CommandWord::is_command(&self.vec[i]) {
break;
}
i += 1;
}
let command = Command::try_from(&self.vec[self.index..i]).ok();
self.index = i;
command
}
}
impl From<Vec<Word>> for CommandVecIntoIterator {
fn from(vec: Vec<Word>) -> Self {
Self { vec, index: 0 }
}
}
pub fn parse_gcode(gcode: &str) -> Vec<Word> {
let mut vec = vec![];
let mut in_string = false;
let mut letter: Option<char> = None;
let mut value_range = 0..0;
gcode.char_indices().for_each(|(i, c)| {
if (c.is_alphabetic() || c.is_ascii_whitespace()) && !in_string {
if let Some(l) = letter {
vec.push(Word {
letter: l,
value: parse_value(&gcode[value_range.clone()]),
});
letter = None;
}
if c.is_alphabetic() {
letter = Some(c);
}
value_range = (i + 1)..(i + 1);
} else if in_string {
value_range = value_range.start..(i + 1);
} else {
if c == '"' {
in_string = !in_string;
}
value_range = value_range.start..(i + 1);
}
});
if let Some(l) = letter {
vec.push(Word {
letter: l,
value: parse_value(&gcode[value_range.clone()]),
});
}
vec
}
fn parse_value(word: &str) -> Value {
if word.starts_with('"') && word.ends_with('"') {
Value::String(Box::new(word.to_string()))
} else {
let index_of_dot = word.find('.');
Value::Fractional(
word[..index_of_dot.unwrap_or(word.len())]
.parse::<u32>()
.unwrap(),
index_of_dot.map(|j| word[j + 1..].parse::<u32>().unwrap()),
)
}
}
/// Writes a GCode program or sequence to a Writer
/// Each command is placed on a separate line
pub fn program2gcode<W: Write>(program: Vec<Command>, mut w: W) -> io::Result<()> {
for command in program.into_iter() {
let words: Vec<Word> = command.into();
let mut it = words.iter();
if let Some(command_word) = it.next() {
write!(w, "{}{}", command_word.letter, command_word.value)?;
for word in it {
write!(w, " {}{} ", word.letter, word.value)?;
}
writeln!(w, "")?;
}
}
Ok(())
}

@ -1,69 +1,48 @@
use core::convert::TryFrom;
use std::io::{self, Write};
use std::convert::TryFrom;
/// Collapses GCode words into higher-level commands
/// Relies on the first word being a command.
pub struct CommandVec {
pub inner: Vec<Word>,
}
impl Default for CommandVec {
fn default() -> Self {
Self {
inner: vec![]
}
}
}
impl IntoIterator for CommandVec {
type Item = Command;
type IntoIter = CommandVecIntoIterator;
fn into_iter(self) -> Self::IntoIter {
CommandVecIntoIterator {
vec: self,
index: 0,
}
}
/// Fundamental unit of GCode: a value preceded by a descriptive letter.
#[derive(Clone, PartialEq, Debug)]
pub struct Word {
pub letter: char,
pub value: Value,
}
pub struct CommandVecIntoIterator {
vec: CommandVec,
index: usize,
/// All the possible variations of a word's value.
/// Fractional is needed to support commands like G91.1 which would be changed by float arithmetic.
/// Some flavors of GCode also allow for strings.
#[derive(Clone, PartialEq, Debug)]
pub enum Value {
Fractional(u32, Option<u32>),
Float(f64),
String(Box<String>)
}
impl Iterator for CommandVecIntoIterator {
type Item = Command;
fn next(&mut self) -> Option<Self::Item> {
if self.vec.inner.len() == self.index {
return None;
}
let mut i = self.index + 1;
while i < self.vec.inner.len() {
if CommandWord::is_command(&self.vec.inner[i]) {
break;
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)
}
i += 1;
}
Command::try_from(&self.vec.inner[self.index..i]).ok()
}
}
/// Fundamental unit of GCode: a value preceded by a descriptive letter.
/// A float is used here to encompass all the possible variations of a value.
/// Some flavors of GCode may allow strings, but that is currently not supported.
#[derive(Clone, PartialEq, Debug)]
pub struct Word {
pub letter: char,
pub value: f64,
}
/// A macro for quickly instantiating a float-valued command
#[macro_export]
macro_rules! command {
($commandWord: expr, {$($argument: ident : $value: expr,)*}) => {
paste::expr! (Command::new($commandWord, vec![$(Word {
letter: stringify!([<$argument:upper>]).chars().next().unwrap(),
value: $value as f64,
value: Value::Float($value),
},)*]))
};
}
@ -124,6 +103,9 @@ macro_rules! commands {
impl TryFrom<&[Word]> for Command {
type Error = ();
fn try_from(words: &[Word]) -> Result<Self, ()> {
if words.len() == 0 {
return Err(());
}
let command_word = CommandWord::try_from(&words[0])?;
let mut arguments = Vec::with_capacity(words.len() - 1);
for i in 1..words.len() {
@ -162,13 +144,9 @@ macro_rules! commands {
paste::item! {
impl CommandWord {
pub fn is_command(word: &Word) -> bool {
let number = word.value as u16;
let fraction_numeric =
f64::from_bits(word.value.fract().to_bits() & 0x00_00_FF_FF_FF_FF_FF_FF) as u16;
let fraction = if fraction_numeric == 0 {
None
} else {
Some(fraction_numeric)
let (number, fraction) = match &word.value {
Value::Fractional(number, fraction) => (number, fraction),
_other => return false
};
match (word.letter, number, fraction) {
$(($letter, $number, $fraction) => true,)*
@ -183,18 +161,14 @@ macro_rules! commands {
impl TryFrom<&Word> for CommandWord {
type Error = ();
fn try_from(word: &Word) -> Result<Self, ()> {
let number = word.value as u16;
let fraction_numeric =
f64::from_bits(word.value.fract().to_bits() & 0x00_00_FF_FF_FF_FF_FF_FF) as u16;
let fraction = if fraction_numeric == 0 {
None
} else {
Some(fraction_numeric)
let (number, fraction) = match &word.value {
Value::Fractional(number, fraction) => (number, fraction),
_other => return Err(())
};
match (word.letter, number, fraction) {
$(($letter, $number, $fraction) => Ok(Self::$commandName),)*
('*', checksum, None) => Ok(Self::Checksum(checksum as u8)),
('N', line_number, None) => Ok(Self::LineNumber(line_number)),
('*', checksum, None) => Ok(Self::Checksum(*checksum as u8)),
('N', line_number, None) => Ok(Self::LineNumber(*line_number as u16)),
(_, _, _) => Err(())
}
}
@ -208,20 +182,20 @@ macro_rules! commands {
Self::$commandName {} => Word {
letter: $letter,
// TODO: fix fraction
value: $number as f64 + ($fraction.unwrap_or(0) as f64)
value: Value::Fractional($number, $fraction)
},
)*
Self::Checksum(value) => Word {
letter: '*',
value: value as f64
value: Value::Fractional(value as u32, None)
},
Self::LineNumber(value) => Word {
letter: 'N',
value: value as f64
value: Value::Fractional(value as u32, None)
},
Self::Comment(_string) => Word {
Self::Comment(string) => Word {
letter: ';',
value: 0.0
value: Value::String(string)
}
}
}
@ -288,6 +262,9 @@ commands!(
RelativeDistanceMode {
'G', 91, None, {}
},
FeedRateUnitsPerMinute {
'G', 94, None, {}
},
/// Start spinning the spindle clockwise with speed `p`
StartSpindleClockwise {
'M', 3, None, {
@ -311,33 +288,3 @@ commands!(
'M', 20, None, {}
},
);
/// Rudimentary regular expression GCode validator
pub fn validate_gcode(gcode: &&str) -> bool {
use regex::Regex;
let re = Regex::new(r##"^(?:(?:%|\(.*\)|(?:[A-Z^E^U][+-]?\d+(?:\.\d*)?))\h*)*$"##).unwrap();
gcode.lines().all(|line| re.is_match(line))
}
/// Writes a GCode program (or sequence) to a Writer
pub fn program2gcode<W: Write>(program: Vec<Command>, mut w: W) -> io::Result<()> {
for command in program.into_iter() {
match &command.command_word {
CommandWord::Comment(string) => {
writeln!(w, ";{}", string)?;
},
_other => {
let words: Vec<Word> = command.into();
let mut it = words.iter();
if let Some(command_word) = it.next() {
write!(w, "{}{}", command_word.letter, command_word.value)?;
for word in it {
write!(w, " {}{} ", word.letter, word.value)?;
}
writeln!(w, "")?;
}
}
}
}
Ok(())
}

@ -29,23 +29,32 @@ pub struct Machine {
distance_mode: Option<Distance>,
tool_on_action: Vec<Command>,
tool_off_action: Vec<Command>,
program_begin_sequence: Vec<Command>,
program_end_sequence: Vec<Command>,
}
impl Machine {
/// Create a generic machine, given a tool on/off GCode sequence.
pub fn new(tool_on_action: CommandVec, tool_off_action: CommandVec) -> Self {
pub fn new(
tool_on_action: Vec<Word>,
tool_off_action: Vec<Word>,
program_begin_sequence: Vec<Word>,
program_end_sequence: Vec<Word>,
) -> Self {
Self {
tool_state: None,
distance_mode: None,
tool_on_action: tool_on_action.into_iter().collect(),
tool_off_action: tool_off_action.into_iter().collect()
tool_on_action: CommandVecIntoIterator::from(tool_on_action).collect(),
tool_off_action: CommandVecIntoIterator::from(tool_off_action).collect(),
program_begin_sequence: CommandVecIntoIterator::from(program_begin_sequence).collect(),
program_end_sequence: CommandVecIntoIterator::from(program_end_sequence).collect(),
}
}
}
impl Machine {
/// Output gcode to turn the tool on.
pub fn tool_on(& mut self) -> Vec<Command> {
pub fn tool_on(&mut self) -> Vec<Command> {
if self.tool_state == Some(Tool::Off) || self.tool_state == None {
self.tool_state = Some(Tool::On);
self.tool_on_action.clone()
@ -63,6 +72,10 @@ impl Machine {
vec![]
}
}
pub fn program_begin(&self) -> Vec<Command> { self.program_begin_sequence.clone() }
pub fn program_end(&self) -> Vec<Command> { self.program_end_sequence.clone() }
/// Output relative distance field if mode was absolute or unknown.
pub fn absolute(&mut self) -> Vec<Command> {
if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None {
@ -83,4 +96,3 @@ impl Machine {
}
}
}

@ -1,5 +1,3 @@
/// TODO: documentation
#[macro_use]
extern crate clap;
extern crate env_logger;
@ -26,9 +24,8 @@ use code::*;
use machine::*;
use turtle::*;
// TODO: Documentation
fn main() -> io::Result<()> {
if env::var("RUST_LOG").is_err() {
if let Err(_) = env::var("RUST_LOG") {
env::set_var("RUST_LOG", "svg2gcode=info")
}
env_logger::init();
@ -36,12 +33,15 @@ fn main() -> io::Result<()> {
(version: crate_version!())
(author: crate_authors!())
(about: crate_description!())
(@arg FILE: "Selects the input SVG file to use, else reading from stdin")
(@arg tolerance: --tolerance "Sets the interpolation tolerance for curves")
(@arg feedrate: --feedrate "Sets the machine feed rate")
(@arg dpi: --dpi "Sets the DPI for SVGs with units in pt, pc, etc. (default 72.0)")
(@arg tool_on_action: --tool_on_action "Sets the tool on GCode sequence")
(@arg tool_off_action: --tool_off_action "Sets the tool off GCode sequence")
(@arg FILE: "A file path for an SVG, else reads from stdin")
(@arg tolerance: --tolerance +takes_value "Curve interpolation tolerance (default: 0.002mm)")
(@arg feedrate: --feedrate +takes_value "Machine feed rate in mm/min (default: 300mm/min)")
(@arg dpi: --dpi +takes_value "Dots per inch (DPI) for pixels, points, picas, etc. (default: 96dpi)")
(@arg tool_on_sequence: --on +takes_value +required "Tool on GCode sequence")
(@arg tool_off_sequence: --off +takes_value +required "Tool off GCode sequence")
(@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")
)
.get_matches();
@ -54,68 +54,69 @@ fn main() -> io::Result<()> {
input
}
None => {
info!("Reading from stdin");
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
input
}
};
let mut opts = ProgramOptions::default();
let mut mach = Machine::new(CommandVec::default(), CommandVec::default());
if let Some(tolerance) = matches.value_of("tolerance").and_then(|x| x.parse().ok()) {
opts.tolerance = tolerance;
}
if let Some(feedrate) = matches.value_of("feedrate").and_then(|x| x.parse().ok()) {
opts.feedrate = feedrate;
}
if let Some(dpi) = matches.value_of("dpi").and_then(|x| x.parse().ok()) {
opts.dpi = dpi;
}
let opts = ProgramOptions {
tolerance: matches
.value_of("tolerance")
.map(|x| x.parse().expect("could not parse tolerance"))
.unwrap_or(0.002),
feedrate: matches
.value_of("feedrate")
.map(|x| x.parse().expect("could not parse feedrate"))
.unwrap_or(300.0),
dpi: matches
.value_of("dpi")
.map(|x| x.parse().expect("could not parse DPI"))
.unwrap_or(96.0),
};
// if let Some(tool_on_action) = matches.value_of("tool_on_action").filter(validate_gcode) {
// mach.tool_on_action = vec![GCode::Raw(Box::new(tool_on_action.to_string()))];
// }
// if let Some(tool_off_action) = matches.value_of("tool_off_action").filter(validate_gcode) {
// mach.tool_off_action = vec![GCode::Raw(Box::new(tool_off_action.to_string()))];
// }
let mach = 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(),
matches
.value_of("begin_sequence")
.map(parse_gcode)
.unwrap_or_default(),
matches.value_of("end_sequence").map(parse_gcode).unwrap_or_default(),
);
let doc = svgdom::Document::from_str(&input).expect("Invalid or unsupported SVG file");
let prog = svg2program(&doc, opts, mach);
program2gcode(prog, File::create("out.gcode")?)
if let Some(out_path) = matches.value_of("out") {
program2gcode(prog, File::create(out_path)?)
} else {
program2gcode(prog, std::io::stdout())
}
}
// TODO: Documentation
/// High-level output options
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,
}
// Sets the baseline options for the machine.
impl Default for ProgramOptions {
fn default() -> Self {
ProgramOptions {
tolerance: 0.002, // See https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration#12--arc-tolerance-mm
feedrate: 3000.0,
dpi: 72.0,
}
}
}
// TODO: Documentation
// TODO: This function is much too large
fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> Vec<Command> {
let mut p = vec![];
let mut t = Turtle::new(mach);
let mut p = vec![
command!(CommandWord::UnitsMillimeters, {}),
command!(CommandWord::FeedRateUnitsPerMinute, {}),
];
let mut namestack: Vec<String> = vec![];
p.push(command!(CommandWord::UnitsMillimeters, {}));
p.append(&mut t.mach.tool_off());
p.append(&mut
t.move_to(true, 0.0, 0.0));
p.append(&mut t.mach.program_begin());
p.append(&mut t.mach.absolute());
p.append(&mut t.move_to(true, 0.0, 0.0));
for edge in doc.root().traverse() {
let (node, is_start) = match edge {
@ -193,8 +194,8 @@ fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> V
for segment in path.iter() {
p.append(&mut match segment {
PathSegment::MoveTo { abs, x, y } => t.move_to(*abs, *x, *y),
PathSegment::ClosePath { abs } => {
// Ignore abs, should have identical effect: https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
PathSegment::ClosePath { abs: _ } => {
// Ignore abs, should have identical effect: [9.3.4. The "closepath" command]("https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand)
t.close(None, opts.feedrate)
}
PathSegment::LineTo { abs, x, y } => {
@ -291,16 +292,17 @@ fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> V
p.append(&mut t.mach.tool_off());
p.append(&mut t.mach.absolute());
p.push(command!(CommandWord::RapidPositioning, {
x: 0.0,
y: 0.0,
}));
p.append(&mut t.move_to(true, 0.0, 0.0));
p.append(&mut t.mach.program_end());
p.push(command!(CommandWord::ProgramEnd, {}));
p
}
// TODO: Documentation
/// 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;
@ -310,9 +312,16 @@ fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 {
Cm => Length::new::<centimeter>(l.num),
Mm => Length::new::<millimeter>(l.num),
In => Length::new::<inch>(l.num),
Pt => Length::new::<point_printers>(l.num) * dpi / 72.0, // See https://github.com/iliekturtles/uom/blob/5cad47d4e67c902304c4c2b7feeb9c3d34fdffba/src/si/length.rs#L61
Pc => Length::new::<pica_printers>(l.num) * dpi / 72.0, // See https://github.com/iliekturtles/uom/blob/5cad47d4e67c902304c4c2b7feeb9c3d34fdffba/src/si/length.rs#L58
_ => Length::new::<millimeter>(l.num),
Pc => Length::new::<pica_computer>(l.num) * dpi / 96.0,
Pt => Length::new::<point_computer>(l.num) * dpi / 96.0,
Px => Length::new::<inch>(l.num * dpi / 96.0),
other => {
warn!(
"Converting from '{:?}' to millimeters is not supported, treating as millimeters",
other
);
Length::new::<millimeter>(l.num)
}
};
length.get::<millimeter>()

@ -77,13 +77,13 @@ impl Turtle {
if let Some(z) = z {
linear_interpolation.push(Word {
letter: 'Z',
value: z,
value: Value::Float(z),
});
}
if let Some(f) = f {
linear_interpolation.push(Word {
letter: 'F',
value: f,
value: Value::Float(f),
});
}
linear_interpolation

Loading…
Cancel
Save