make setting the origin a pre-processing step, fixes #23

master
Sameer Puri 4 years ago
parent 433c4c7482
commit 171387643e

2
Cargo.lock generated

@ -633,7 +633,7 @@ dependencies = [
[[package]] [[package]]
name = "svg2gcode" name = "svg2gcode"
version = "0.0.6" version = "0.0.7"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"euclid", "euclid",

@ -12,9 +12,7 @@ use std::{
use structopt::StructOpt; use structopt::StructOpt;
use svgtypes::LengthListParser; use svgtypes::LengthListParser;
use svg2gcode::{ use svg2gcode::{svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality};
set_origin, svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality,
};
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
#[structopt(name = "svg2gcode", author, about)] #[structopt(name = "svg2gcode", author, about)]
@ -113,7 +111,6 @@ fn main() -> io::Result<()> {
} }
} }
{ {
let postprocess = &mut settings.postprocess;
if let Some(origin) = opt.origin { if let Some(origin) = opt.origin {
for (i, dimension_origin) in origin for (i, dimension_origin) in origin
.split(',') .split(',')
@ -121,13 +118,13 @@ fn main() -> io::Result<()> {
if point.is_empty() { if point.is_empty() {
Default::default() Default::default()
} else { } else {
point.parse().expect("could not parse coordinate") point.parse::<f64>().expect("could not parse coordinate")
} }
}) })
.take(2) .take(2)
.enumerate() .enumerate()
{ {
postprocess.origin[i] = dimension_origin; settings.conversion.origin[i] = Some(dimension_origin);
} }
} }
} }
@ -135,11 +132,11 @@ fn main() -> io::Result<()> {
}; };
if let Some(export_path) = opt.export { if let Some(export_path) = opt.export {
let mut config_json_bytes = serde_json::to_vec_pretty(&settings)?; let config_json_bytes = serde_json::to_vec_pretty(&settings)?;
if export_path.to_string_lossy() == "-" { if export_path.to_string_lossy() == "-" {
return io::stdout().write_all(&mut config_json_bytes); return io::stdout().write_all(&config_json_bytes);
} else { } else {
return File::create(export_path)?.write_all(&mut config_json_bytes); return File::create(export_path)?.write_all(&config_json_bytes);
} }
} }
@ -253,9 +250,7 @@ fn main() -> io::Result<()> {
let document = roxmltree::Document::parse(&input).unwrap(); let document = roxmltree::Document::parse(&input).unwrap();
let mut program = svg2program(&document, &settings.conversion, options, machine); let program = svg2program(&document, &settings.conversion, options, machine);
set_origin(&mut program, settings.postprocess.origin);
if let Some(out_path) = opt.out { if let Some(out_path) = opt.out {
format_gcode_io(&program, FormatOptions::default(), File::create(out_path)?) format_gcode_io(&program, FormatOptions::default(), File::create(out_path)?)

@ -1,6 +1,6 @@
[package] [package]
name = "svg2gcode" name = "svg2gcode"
version = "0.0.6" version = "0.0.7"
authors = ["Sameer Puri <crates@purisa.me>"] authors = ["Sameer Puri <crates@purisa.me>"]
edition = "2018" edition = "2018"
description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine." description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine."

@ -11,7 +11,7 @@ where
{ {
let mut seq = serializer.serialize_seq(Some(2))?; let mut seq = serializer.serialize_seq(Some(2))?;
for i in 0..2 { for i in 0..2 {
let length_def = length[i].clone().map(|length| LengthDef { let length_def = length[i].map(|length| LengthDef {
number: length.number, number: length.number,
unit: length.unit, unit: length.unit,
}); });

@ -1,7 +1,7 @@
use std::fmt::Debug;
use std::str::FromStr; use std::str::FromStr;
use std::{borrow::Cow, fmt::Debug};
use g_code::{command, emit::Token}; use g_code::emit::Token;
use log::{debug, warn}; use log::{debug, warn};
use lyon_geom::{ use lyon_geom::{
euclid::{default::Transform2D, Angle, Transform3D}, euclid::{default::Transform2D, Angle, Transform3D},
@ -35,6 +35,13 @@ pub struct ConversionConfig {
pub feedrate: f64, pub feedrate: f64,
/// Dots per inch for pixels, picas, points, etc. /// Dots per inch for pixels, picas, points, etc.
pub dpi: f64, pub dpi: f64,
/// Set the origin point for this conversion
#[cfg_attr(feature = "serde", serde(default = "zero_origin"))]
pub origin: [Option<f64>; 2],
}
const fn zero_origin() -> [Option<f64>; 2] {
[Some(0.); 2]
} }
impl Default for ConversionConfig { impl Default for ConversionConfig {
@ -43,6 +50,7 @@ impl Default for ConversionConfig {
tolerance: 0.002, tolerance: 0.002,
feedrate: 300.0, feedrate: 300.0,
dpi: 96.0, dpi: 96.0,
origin: [Some(0.); 2],
} }
} }
} }
@ -70,26 +78,31 @@ struct ConversionVisitor<'a, T: Turtle> {
impl<'a, 'input: 'a> ConversionVisitor<'a, GCodeTurtle<'input>> { impl<'a, 'input: 'a> ConversionVisitor<'a, GCodeTurtle<'input>> {
fn begin(&mut self) { fn begin(&mut self) {
// Part 1 of converting from SVG to g-code coordinates
self.terrarium.push_transform(Transform2D::scale(1., -1.));
self.terrarium.turtle.begin(); self.terrarium.turtle.begin();
}
fn end(&mut self) {
self.terrarium.pop_transform();
self.terrarium.turtle.end();
}
}
impl<'a, 'input: 'a> ConversionVisitor<'a, PreprocessTurtle> {
fn begin(&mut self) {
// Part 1 of converting from SVG to g-code coordinates // Part 1 of converting from SVG to g-code coordinates
self.terrarium.push_transform(Transform2D::scale(1., -1.)); self.terrarium.push_transform(Transform2D::scale(1., -1.));
} }
fn end(&mut self) { fn end(&mut self) {
self.terrarium.pop_all_transforms(); self.terrarium.pop_transform();
self.terrarium.turtle.end();
} }
} }
impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> { impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> {
fn visit(&mut self, node: Node) { fn visit(&mut self, node: Node) {
// Depth-first SVG DOM traversal
if node.node_type() != roxmltree::NodeType::Element {
debug!("Encountered a non-element: {:?}", node);
}
if node.tag_name().name() == CLIP_PATH_TAG_NAME { if node.tag_name().name() == CLIP_PATH_TAG_NAME {
warn!("Clip paths are not supported: {:?}", node); warn!("Clip paths are not supported: {:?}", node);
} }
@ -198,13 +211,13 @@ impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> {
}); });
comment += &node_name(&node); comment += &node_name(&node);
self.terrarium.turtle.comment(comment); self.terrarium.turtle.comment(comment);
apply_path(&mut self.terrarium, &self.config, d); apply_path(&mut self.terrarium, 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.first_element_child().is_some() {
self.name_stack.push(node_name(&node)); self.name_stack.push(node_name(&node));
} else { } else {
// Pop transform since this is the only element that has it // Pop transform since this is the only element that has it
@ -212,13 +225,16 @@ impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> {
let mut parent = Some(node); let mut parent = Some(node);
while let Some(p) = parent { while let Some(p) = parent {
if p.next_sibling().is_some() || p.is_root() { if p.next_sibling_element().is_some()
|| p.is_root()
|| p.tag_name().name() == SVG_TAG_NAME
{
break; break;
} }
// Pop the parent transform since this is the last child // Pop the parent transform since this is the last child
self.terrarium.pop_transform(); self.terrarium.pop_transform();
self.name_stack.pop(); self.name_stack.pop();
parent = p.parent(); parent = p.parent_element();
} }
} }
} }
@ -230,6 +246,32 @@ pub fn svg2program<'a, 'input: 'a>(
options: ConversionOptions, options: ConversionOptions,
machine: Machine<'input>, machine: Machine<'input>,
) -> Vec<Token<'input>> { ) -> Vec<Token<'input>> {
let bounding_box = {
let mut visitor = ConversionVisitor {
terrarium: Terrarium::new(PreprocessTurtle::default()),
config,
options: options.clone(),
name_stack: vec![],
};
visitor.begin();
visit::depth_first_visit(doc, &mut visitor);
visitor.end();
visitor.terrarium.turtle.bounding_box
};
let origin_transform = {
let mut transform = Transform2D::identity();
if let Some(origin_x) = config.origin[0] {
transform = transform.then_translate(vector(origin_x - bounding_box.min.x, 0.));
}
if let Some(origin_y) = config.origin[1] {
transform = transform.then_translate(vector(0., origin_y - bounding_box.min.y));
}
transform
};
let mut conversion_visitor = ConversionVisitor { let mut conversion_visitor = ConversionVisitor {
terrarium: Terrarium::new(GCodeTurtle { terrarium: Terrarium::new(GCodeTurtle {
machine, machine,
@ -241,9 +283,13 @@ pub fn svg2program<'a, 'input: 'a>(
options, options,
name_stack: vec![], name_stack: vec![],
}; };
conversion_visitor
.terrarium
.push_transform(origin_transform);
conversion_visitor.begin(); conversion_visitor.begin();
visit::depth_first_visit(doc, &mut conversion_visitor); visit::depth_first_visit(doc, &mut conversion_visitor);
conversion_visitor.end(); conversion_visitor.end();
conversion_visitor.terrarium.pop_transform();
conversion_visitor.terrarium.turtle.program conversion_visitor.terrarium.turtle.program
} }
@ -257,25 +303,21 @@ fn node_name(node: &Node) -> String {
name name
} }
fn apply_path<'input, T: Turtle + Debug>( fn apply_path<T: Turtle + Debug>(terrarium: &mut Terrarium<T>, path: &str) {
turtle: &mut Terrarium<T>,
config: &ConversionConfig,
path: &str,
) {
use PathSegment::*; use PathSegment::*;
PathParser::from(path) PathParser::from(path)
.map(|segment| segment.expect("could not parse path segment")) .map(|segment| segment.expect("could not parse path segment"))
.for_each(|segment| { .for_each(|segment| {
debug!("Drawing {:?}", &segment); debug!("Drawing {:?}", &segment);
match segment { match segment {
MoveTo { abs, x, y } => turtle.move_to(abs, x, y), MoveTo { abs, x, y } => terrarium.move_to(abs, x, y),
ClosePath { abs: _ } => { ClosePath { abs: _ } => {
// Ignore abs, should have identical effect: [9.3.4. The "closepath" command]("https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand) // Ignore abs, should have identical effect: [9.3.4. The "closepath" command]("https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand)
turtle.close() terrarium.close()
} }
LineTo { abs, x, y } => turtle.line(abs, x, y), LineTo { abs, x, y } => terrarium.line(abs, x, y),
HorizontalLineTo { abs, x } => turtle.line(abs, x, None), HorizontalLineTo { abs, x } => terrarium.line(abs, x, None),
VerticalLineTo { abs, y } => turtle.line(abs, None, y), VerticalLineTo { abs, y } => terrarium.line(abs, None, y),
CurveTo { CurveTo {
abs, abs,
x1, x1,
@ -284,14 +326,16 @@ fn apply_path<'input, T: Turtle + Debug>(
y2, y2,
x, x,
y, y,
} => turtle.cubic_bezier(abs, point(x1, y1), point(x2, y2), point(x, y)), } => terrarium.cubic_bezier(abs, point(x1, y1), point(x2, y2), point(x, y)),
SmoothCurveTo { abs, x2, y2, x, y } => { SmoothCurveTo { abs, x2, y2, x, y } => {
turtle.smooth_cubic_bezier(abs, point(x2, y2), point(x, y)) terrarium.smooth_cubic_bezier(abs, point(x2, y2), point(x, y))
} }
Quadratic { abs, x1, y1, x, y } => { Quadratic { abs, x1, y1, x, y } => {
turtle.quadratic_bezier(abs, point(x1, y1), point(x, y)) terrarium.quadratic_bezier(abs, point(x1, y1), point(x, y))
}
SmoothQuadratic { abs, x, y } => {
terrarium.smooth_quadratic_bezier(abs, point(x, y))
} }
SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier(abs, point(x, y)),
EllipticalArc { EllipticalArc {
abs, abs,
rx, rx,
@ -301,7 +345,7 @@ fn apply_path<'input, T: Turtle + Debug>(
sweep, sweep,
x, x,
y, y,
} => turtle.elliptical( } => terrarium.elliptical(
abs, abs,
vector(rx, ry), vector(rx, ry),
Angle::degrees(x_axis_rotation), Angle::degrees(x_axis_rotation),

@ -5,9 +5,14 @@ pub trait XmlVisitor {
} }
pub fn depth_first_visit(doc: &Document, visitor: &mut impl XmlVisitor) { pub fn depth_first_visit(doc: &Document, visitor: &mut impl XmlVisitor) {
let mut stack = doc.root().children().rev().collect::<Vec<_>>(); let mut stack = doc
.root()
.children()
.rev()
.filter(|x| x.is_element())
.collect::<Vec<_>>();
while let Some(node) = stack.pop() { while let Some(node) = stack.pop() {
visitor.visit(node); visitor.visit(node);
stack.extend(node.children().rev()); stack.extend(node.children().rev().filter(|x| x.is_element()));
} }
} }

@ -51,8 +51,7 @@ mod test {
None, None,
None, None,
); );
let mut program = converter::svg2program(&document, &config, options, machine); let program = converter::svg2program(&document, &config, options, machine);
postprocess::set_origin(&mut program, [0., 0.]);
let mut acc = String::new(); let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap(); format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();
@ -133,4 +132,34 @@ mod test {
include_str!("../tests/smooth_curves_circular_interpolation.gcode") include_str!("../tests/smooth_curves_circular_interpolation.gcode")
); );
} }
#[test]
#[cfg(feature = "serde")]
fn deserialize_v1_config_succeeds() {
let json = r#"
{
"conversion": {
"tolerance": 0.002,
"feedrate": 300.0,
"dpi": 96.0
},
"machine": {
"supported_functionality": {
"circular_interpolation": true
},
"tool_on_sequence": null,
"tool_off_sequence": null,
"begin_sequence": null,
"end_sequence": null
},
"postprocess": {
"origin": [
0.0,
0.0
]
}
}
"#;
serde_json::from_str::<Settings>(json).unwrap();
}
} }

@ -13,10 +13,18 @@ type F64Point = Point<f64>;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Default, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
pub struct PostprocessConfig { pub struct PostprocessConfig {
#[deprecated(
since = "0.7.0",
note = "Setting the origin is now a preprocessing operation"
)]
pub origin: [f64; 2], pub origin: [f64; 2],
} }
/// Moves all the commands so that they are beyond a specified position /// Moves all the commands so that they are beyond a specified position
#[deprecated(
since = "0.7.0",
note = "Setting the origin is now a preprocessing operation"
)]
pub fn set_origin(tokens: &mut [Token<'_>], origin: [f64; 2]) { pub fn set_origin(tokens: &mut [Token<'_>], origin: [f64; 2]) {
let offset = let offset =
-get_bounding_box(tokens.iter()).min.to_vector() + F64Point::from(origin).to_vector(); -get_bounding_box(tokens.iter()).min.to_vector() + F64Point::from(origin).to_vector();

@ -2,12 +2,11 @@ use std::borrow::Cow;
use std::fmt::Debug; use std::fmt::Debug;
use ::g_code::{command, emit::Token}; use ::g_code::{command, emit::Token};
use lyon_geom::Point; use lyon_geom::{CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc};
use lyon_geom::{CubicBezierSegment, QuadraticBezierSegment, SvgArc};
use super::Turtle;
use crate::arc::{ArcOrLineSegment, FlattenWithArcs}; use crate::arc::{ArcOrLineSegment, FlattenWithArcs};
use crate::machine::Machine; use crate::machine::Machine;
use super::Turtle;
/// Turtle graphics simulator for mapping path segments into g-code /// Turtle graphics simulator for mapping path segments into g-code
#[derive(Debug)] #[derive(Debug)]

@ -1,13 +1,16 @@
use std::fmt::Debug; use std::fmt::Debug;
use lyon_geom::euclid::{default::Transform2D, Angle}; use lyon_geom::{
use lyon_geom::{point, vector, Point, Vector}; euclid::{default::Transform2D, Angle},
use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc}; point, vector, ArcFlags, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc, Vector,
};
use crate::arc::{Transformed}; use crate::arc::Transformed;
mod g_code; mod g_code;
mod preprocess;
pub use self::g_code::GCodeTurtle; pub use self::g_code::GCodeTurtle;
pub use preprocess::PreprocessTurtle;
pub trait Turtle: Debug { pub trait Turtle: Debug {
fn begin(&mut self); fn begin(&mut self);
@ -27,7 +30,7 @@ pub struct Terrarium<T: Turtle + std::fmt::Debug> {
current_position: Point<f64>, current_position: Point<f64>,
initial_position: Point<f64>, initial_position: Point<f64>,
current_transform: Transform2D<f64>, current_transform: Transform2D<f64>,
transform_stack: Vec<Transform2D<f64>>, pub transform_stack: Vec<Transform2D<f64>>,
previous_quadratic_control: Option<Point<f64>>, previous_quadratic_control: Option<Point<f64>>,
previous_cubic_control: Option<Point<f64>>, previous_cubic_control: Option<Point<f64>>,
} }

@ -0,0 +1,40 @@
use lyon_geom::{Box2D, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc};
use super::Turtle;
#[derive(Debug, Default)]
pub struct PreprocessTurtle {
pub bounding_box: Box2D<f64>,
}
impl Turtle for PreprocessTurtle {
fn begin(&mut self) {}
fn end(&mut self) {}
fn comment(&mut self, _comment: String) {}
fn move_to(&mut self, to: Point<f64>) {
self.bounding_box = Box2D::from_points([self.bounding_box.min, self.bounding_box.max, to]);
}
fn line_to(&mut self, to: Point<f64>) {
self.bounding_box = Box2D::from_points([self.bounding_box.min, self.bounding_box.max, to]);
}
fn arc(&mut self, svg_arc: SvgArc<f64>) {
if svg_arc.is_straight_line() {
self.line_to(svg_arc.to);
} else {
self.bounding_box = self.bounding_box.union(&svg_arc.to_arc().bounding_box());
}
}
fn cubic_bezier(&mut self, cbs: CubicBezierSegment<f64>) {
self.bounding_box = self.bounding_box.union(&cbs.bounding_box());
}
fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment<f64>) {
self.bounding_box = self.bounding_box.union(&qbs.bounding_box());
}
}

@ -6,7 +6,7 @@ use g_code::{
}; };
use log::Level; use log::Level;
use roxmltree::Document; use roxmltree::Document;
use svg2gcode::{set_origin, svg2program, ConversionOptions, Machine, Turtle}; use svg2gcode::{svg2program, ConversionOptions, Machine};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::{Dispatch, Dispatcher}; use yewdux::prelude::{Dispatch, Dispatcher};
@ -111,15 +111,13 @@ impl Component for App {
); );
let document = Document::parse(svg.content.as_str()).unwrap(); let document = Document::parse(svg.content.as_str()).unwrap();
let mut program = svg2program( let program = svg2program(
&document, &document,
&app_state.settings.conversion, &app_state.settings.conversion,
options, options,
machine, machine,
); );
set_origin(&mut program, app_state.settings.postprocess.origin);
let gcode = { let gcode = {
let mut acc = String::new(); let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap(); format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();

@ -35,6 +35,7 @@ impl<'a> TryInto<Settings> for &'a FormState {
tolerance: self.tolerance.clone()?, tolerance: self.tolerance.clone()?,
feedrate: self.feedrate.clone()?, feedrate: self.feedrate.clone()?,
dpi: self.dpi.clone()?, dpi: self.dpi.clone()?,
origin: [Some(self.origin[0].clone()?), Some(self.origin[1].clone()?)],
}, },
machine: MachineConfig { machine: MachineConfig {
supported_functionality: SupportedFunctionality { supported_functionality: SupportedFunctionality {
@ -62,8 +63,8 @@ impl From<&Settings> for FormState {
.supported_functionality .supported_functionality
.circular_interpolation, .circular_interpolation,
origin: [ origin: [
Ok(settings.postprocess.origin[0]), Ok(settings.conversion.origin[0].unwrap_or(settings.postprocess.origin[0])),
Ok(settings.postprocess.origin[1]), Ok(settings.conversion.origin[1].unwrap_or(settings.postprocess.origin[1])),
], ],
dpi: Ok(settings.conversion.dpi), dpi: Ok(settings.conversion.dpi),
tool_on_sequence: settings.machine.tool_on_sequence.clone().map(Result::Ok), tool_on_sequence: settings.machine.tool_on_sequence.clone().map(Result::Ok),

Loading…
Cancel
Save