You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
10 KiB

#[macro_use]
extern crate clap;
extern crate env_logger;
extern crate svgdom;
#[macro_use]
extern crate log;
extern crate lyon_geom;
use std::env;
use std::fs::File;
use std::io::{self, Read};
use lyon_geom::{math, euclid};
use svgdom::{AttributeId, AttributeValue, ElementId, ElementType, PathSegment};
mod code;
mod machine;
mod turtle;
use code::*;
use machine::*;
use turtle::*;
fn main() -> io::Result<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "svg2gcode=info")
}
env_logger::init();
let matches = clap_app!(svg2gcode =>
(version: crate_version!())
(author: crate_authors!())
(about: crate_description!())
(@arg FILE: "Selects the input SVG file to use, else reading from stdin")
(@arg tolerance: "Sets the interpolation tolerance for curves")
(@arg feedrate: "Sets the machine feed rate")
(@arg dpi: "Sets the DPI for SVGs with units in pt, pc, etc.")
)
.get_matches();
let input = match matches.value_of("FILE") {
Some(filename) => {
let mut f = File::open(filename)?;
let len = f.metadata()?.len();
let mut input = String::with_capacity(len as usize + 1);
f.read_to_string(&mut input)?;
input
}
None => {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
input
}
};
let mut opts = ProgramOptions::default();
let mut mach = Machine::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;
}
if true {
mach.tool_on_action = vec![GCode::StopSpindle, GCode::Dwell { p: 1.5 }];
}
if true {
mach.tool_off_action = vec![
GCode::Dwell { p: 0.1 },
GCode::StartSpindle {
d: Direction::Clockwise,
s: 40.0,
},
GCode::Dwell { p: 0.2 },
];
}
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")?)
}
struct ProgramOptions {
tolerance: f64,
feedrate: f64,
dpi: f64,
}
impl Default for ProgramOptions {
fn default() -> Self {
ProgramOptions {
tolerance: 0.001,
feedrate: 3000.0,
dpi: 72.0,
}
}
}
fn svg2program(doc: &svgdom::Document, opts: ProgramOptions, mach: Machine) -> Program {
let mut p = Program::new();
let mut t = Turtle::default();
t.mach = mach;
p.push(GCode::UnitsMillimeters);
p.extend(t.mach.tool_off());
p.extend(t.move_to(true, 0.0, 0.0));
p.extend(t.mach.tool_on());
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 attrs = node.attributes();
if let (ElementId::Svg, true) = (id, is_start) {
if let (Some(&AttributeValue::Length(width)), Some(&AttributeValue::Length(height))) = (
attrs.get_value(AttributeId::Width),
attrs.get_value(AttributeId::Height),
) {
let width_in_mm = length_to_mm(width, opts.dpi);
let height_in_mm = length_to_mm(height, opts.dpi);
t.set_scaling(
euclid::Transform2D::create_scale(
width_in_mm / width.num,
-height_in_mm / height.num,
)
.post_translate(math::vector(0.0, height_in_mm)),
);
}
}
if let (ElementId::G, true) = (id, is_start) {
p.push(GCode::Named(Box::new(node.id().to_string())));
}
if let Some(&AttributeValue::Transform(ref trans)) = attrs.get_value(AttributeId::Transform)
{
if is_start {
t.push_transform(lyon_geom::euclid::Transform2D::row_major(
trans.a, trans.b, trans.c, trans.d, trans.e, trans.f,
));
} else {
t.pop_transform();
}
}
if node.is_graphic() && is_start {
match id {
ElementId::Path => {
if let Some(&AttributeValue::Path(ref path)) = attrs.get_value(AttributeId::D) {
p.push(GCode::Named(Box::new(node.id().to_string())));
t.reset();
for segment in path.iter() {
match segment {
PathSegment::MoveTo { abs, x, y } => {
p.extend(t.move_to(*abs, *x, *y))
}
PathSegment::ClosePath { abs } => {
// Ignore abs, should have identical effect: https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
p.extend(t.close(None, opts.feedrate))
}
PathSegment::LineTo { abs, x, y } => {
p.extend(t.line(*abs, *x, *y, None, opts.feedrate));
}
PathSegment::HorizontalLineTo { abs, x } => {
p.extend(t.line(*abs, *x, None, None, opts.feedrate));
}
PathSegment::VerticalLineTo { abs, y } => {
p.extend(t.line(*abs, None, *y, None, opts.feedrate));
}
PathSegment::CurveTo {
abs,
x1,
y1,
x2,
y2,
x,
y,
} => {
p.extend(t.cubic_bezier(
*abs,
*x1,
*y1,
*x2,
*y2,
*x,
*y,
opts.tolerance,
None,
opts.feedrate,
));
}
PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => {
p.extend(t.smooth_cubic_bezier(
*abs,
*x2,
*y2,
*x,
*y,
opts.tolerance,
None,
opts.feedrate,
));
}
PathSegment::Quadratic { abs, x1, y1, x, y } => {
p.extend(t.quadratic_bezier(
*abs,
*x1,
*y1,
*x,
*y,
opts.tolerance,
None,
opts.feedrate,
));
}
PathSegment::SmoothQuadratic { abs, x, y } => {
p.extend(t.smooth_quadratic_bezier(
*abs,
*x,
*y,
opts.tolerance,
None,
opts.feedrate,
));
}
PathSegment::EllipticalArc {
abs,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
x,
y,
} => {
p.extend(t.elliptical(
*abs,
*rx,
*ry,
*x_axis_rotation,
*large_arc,
*sweep,
*x,
*y,
None,
opts.feedrate,
opts.tolerance,
));
}
}
}
}
}
_ => {
info!("Node <{} id=\"{}\" .../> is not supported", id, node.id());
}
}
}
}
p.extend(t.mach.tool_off());
p.extend(t.mach.absolute());
p.push(GCode::RapidPositioning {
x: 0.0.into(),
y: 0.0.into(),
});
p.extend(t.mach.tool_on());
p.push(GCode::ProgramEnd);
p
}
fn length_to_mm(l: svgdom::Length, dpi: f64) -> f64 {
use svgdom::LengthUnit::*;
let scale = match l.unit {
Cm => 0.1,
Mm => 1.0,
In => 25.4,
Pt => 25.4 / dpi,
Pc => 25.4 / (6.0 * (dpi / 72.0)),
_ => 1.0,
};
l.num * scale
}