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]]
name = "svg2gcode"
version = "0.0.6"
version = "0.0.7"
dependencies = [
"cairo-rs",
"euclid",

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

@ -1,6 +1,6 @@
[package]
name = "svg2gcode"
version = "0.0.6"
version = "0.0.7"
authors = ["Sameer Puri <crates@purisa.me>"]
edition = "2018"
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))?;
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,
unit: length.unit,
});

@ -1,7 +1,7 @@
use std::fmt::Debug;
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 lyon_geom::{
euclid::{default::Transform2D, Angle, Transform3D},
@ -35,6 +35,13 @@ pub struct ConversionConfig {
pub feedrate: f64,
/// Dots per inch for pixels, picas, points, etc.
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 {
@ -43,6 +50,7 @@ impl Default for ConversionConfig {
tolerance: 0.002,
feedrate: 300.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>> {
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();
}
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
self.terrarium.push_transform(Transform2D::scale(1., -1.));
}
fn end(&mut self) {
self.terrarium.pop_all_transforms();
self.terrarium.turtle.end();
self.terrarium.pop_transform();
}
}
impl<'a, T: Turtle> visit::XmlVisitor for ConversionVisitor<'a, T> {
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 {
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);
self.terrarium.turtle.comment(comment);
apply_path(&mut self.terrarium, &self.config, d);
apply_path(&mut self.terrarium, d);
} else {
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));
} else {
// 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);
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;
}
// Pop the parent transform since this is the last child
self.terrarium.pop_transform();
self.name_stack.pop();
parent = p.parent();
parent = p.parent_element();
}
}
}
@ -230,6 +246,32 @@ pub fn svg2program<'a, 'input: 'a>(
options: ConversionOptions,
machine: Machine<'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 {
terrarium: Terrarium::new(GCodeTurtle {
machine,
@ -241,9 +283,13 @@ pub fn svg2program<'a, 'input: 'a>(
options,
name_stack: vec![],
};
conversion_visitor
.terrarium
.push_transform(origin_transform);
conversion_visitor.begin();
visit::depth_first_visit(doc, &mut conversion_visitor);
conversion_visitor.end();
conversion_visitor.terrarium.pop_transform();
conversion_visitor.terrarium.turtle.program
}
@ -257,25 +303,21 @@ fn node_name(node: &Node) -> String {
name
}
fn apply_path<'input, T: Turtle + Debug>(
turtle: &mut Terrarium<T>,
config: &ConversionConfig,
path: &str,
) {
fn apply_path<T: Turtle + Debug>(terrarium: &mut Terrarium<T>, path: &str) {
use PathSegment::*;
PathParser::from(path)
.map(|segment| segment.expect("could not parse path segment"))
.for_each(|segment| {
debug!("Drawing {:?}", &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: _ } => {
// 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),
HorizontalLineTo { abs, x } => turtle.line(abs, x, None),
VerticalLineTo { abs, y } => turtle.line(abs, None, y),
LineTo { abs, x, y } => terrarium.line(abs, x, y),
HorizontalLineTo { abs, x } => terrarium.line(abs, x, None),
VerticalLineTo { abs, y } => terrarium.line(abs, None, y),
CurveTo {
abs,
x1,
@ -284,14 +326,16 @@ fn apply_path<'input, T: Turtle + Debug>(
y2,
x,
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 } => {
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 } => {
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 {
abs,
rx,
@ -301,7 +345,7 @@ fn apply_path<'input, T: Turtle + Debug>(
sweep,
x,
y,
} => turtle.elliptical(
} => terrarium.elliptical(
abs,
vector(rx, ry),
Angle::degrees(x_axis_rotation),

@ -5,9 +5,14 @@ pub trait 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() {
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,
);
let mut program = converter::svg2program(&document, &config, options, machine);
postprocess::set_origin(&mut program, [0., 0.]);
let program = converter::svg2program(&document, &config, options, machine);
let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();
@ -133,4 +132,34 @@ mod test {
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))]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct PostprocessConfig {
#[deprecated(
since = "0.7.0",
note = "Setting the origin is now a preprocessing operation"
)]
pub origin: [f64; 2],
}
/// 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]) {
let offset =
-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 ::g_code::{command, emit::Token};
use lyon_geom::Point;
use lyon_geom::{CubicBezierSegment, QuadraticBezierSegment, SvgArc};
use lyon_geom::{CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc};
use super::Turtle;
use crate::arc::{ArcOrLineSegment, FlattenWithArcs};
use crate::machine::Machine;
use super::Turtle;
/// Turtle graphics simulator for mapping path segments into g-code
#[derive(Debug)]

@ -1,13 +1,16 @@
use std::fmt::Debug;
use lyon_geom::euclid::{default::Transform2D, Angle};
use lyon_geom::{point, vector, Point, Vector};
use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc};
use lyon_geom::{
euclid::{default::Transform2D, Angle},
point, vector, ArcFlags, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc, Vector,
};
use crate::arc::{Transformed};
use crate::arc::Transformed;
mod g_code;
mod preprocess;
pub use self::g_code::GCodeTurtle;
pub use preprocess::PreprocessTurtle;
pub trait Turtle: Debug {
fn begin(&mut self);
@ -27,7 +30,7 @@ pub struct Terrarium<T: Turtle + std::fmt::Debug> {
current_position: Point<f64>,
initial_position: Point<f64>,
current_transform: Transform2D<f64>,
transform_stack: Vec<Transform2D<f64>>,
pub transform_stack: Vec<Transform2D<f64>>,
previous_quadratic_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 roxmltree::Document;
use svg2gcode::{set_origin, svg2program, ConversionOptions, Machine, Turtle};
use svg2gcode::{svg2program, ConversionOptions, Machine};
use yew::prelude::*;
use yewdux::prelude::{Dispatch, Dispatcher};
@ -111,15 +111,13 @@ impl Component for App {
);
let document = Document::parse(svg.content.as_str()).unwrap();
let mut program = svg2program(
let program = svg2program(
&document,
&app_state.settings.conversion,
options,
machine,
);
set_origin(&mut program, app_state.settings.postprocess.origin);
let gcode = {
let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();

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

Loading…
Cancel
Save