add support for overriding dimensions (see #16)

master
Sameer Puri 4 years ago
parent ce18a12471
commit c306ef0b05

1
Cargo.lock generated

@ -656,6 +656,7 @@ dependencies = [
"roxmltree",
"structopt",
"svg2gcode",
"svgtypes",
]
[[package]]

@ -15,6 +15,7 @@ g-code = "0"
codespan-reporting = "0.11"
structopt = "0.3"
roxmltree = "0"
svgtypes = "0"
[[bin]]
name = "svg2gcode"

@ -10,6 +10,7 @@ use std::{
path::PathBuf,
};
use structopt::StructOpt;
use svgtypes::LengthListParser;
use svg2gcode::{
set_origin, svg2program, ConversionOptions, Machine, SupportedFunctionality, Turtle,
@ -48,6 +49,13 @@ struct Opt {
/// Coordinates for the bottom left corner of the machine
#[structopt(long, default_value = "0,0")]
origin: String,
/// Override the width and height of the SVG (i.e. 210mm,297mm)
///
/// Useful when the SVG does not specify these (see https://github.com/sameer/svg2gcode/pull/16)
///
/// Passing "210mm," or ",297mm" calculates the missing dimension to conform to the viewBox aspect ratio.
#[structopt(long)]
dimensions: Option<String>,
/// Whether to use circular arcs when generating g-code
///
/// Please check if your machine supports G2/G3 commands before enabling this.
@ -79,10 +87,33 @@ fn main() -> io::Result<()> {
}
};
let mut dimensions = [None, None];
if let Some(dimensions_str) = opt.dimensions {
dimensions_str
.split(',')
.map(|dimension_str| {
if dimension_str.is_empty() {
None
} else {
LengthListParser::from(dimension_str)
.next()
.transpose()
.expect("could not parse dimension")
}
})
.take(2)
.enumerate()
.for_each(|(i, dimension_origin)| {
dimensions[i] = dimension_origin;
});
}
let options = ConversionOptions {
tolerance: opt.tolerance,
feedrate: opt.feedrate,
dpi: opt.dpi,
dimensions,
};
let snippets = [

@ -9,11 +9,14 @@ use lyon_geom::{
};
use roxmltree::{Document, Node};
use svgtypes::{
LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken, ViewBox,
Length, LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken,
ViewBox,
};
use crate::turtle::*;
const SVG_TAG_NAME: &str = "svg";
/// High-level output options
#[derive(Debug)]
pub struct ConversionOptions {
@ -23,6 +26,10 @@ pub struct ConversionOptions {
pub feedrate: f64,
/// Dots per inch for pixels, picas, points, etc.
pub dpi: f64,
/// Width and height override
///
/// Useful when an SVG does not have a set width and height or you want to override it.
pub dimensions: [Option<Length>; 2],
}
impl Default for ConversionOptions {
@ -31,6 +38,7 @@ impl Default for ConversionOptions {
tolerance: 0.002,
feedrate: 300.0,
dpi: 96.0,
dimensions: [None; 2],
}
}
}
@ -61,11 +69,14 @@ pub fn svg2program<'input>(
node_stack.push((parent, children));
child
}
// Last node in this group has been processed
None => {
if parent.has_attribute("viewBox")
|| parent.has_attribute("transform")
|| parent.has_attribute("width")
|| parent.has_attribute("height")
|| (parent.has_tag_name(SVG_TAG_NAME)
&& options.dimensions.iter().any(Option::is_some))
{
turtle.pop_transform();
}
@ -85,18 +96,74 @@ pub fn svg2program<'input>(
}
let mut transforms = vec![];
if let Some(view_box) = node.attribute("viewBox") {
let view_box = ViewBox::from_str(view_box).expect("could not parse viewBox");
transforms.push(
Transform2D::translation(-view_box.x, -view_box.y)
.then_scale(1. / view_box.w, 1. / view_box.h)
// Part 2 of converting from SVG to g-code coordinates
.then_translate(vector(0., -1.)),
let view_box = node
.attribute("viewBox")
.map(ViewBox::from_str)
.transpose()
.expect("could not parse viewBox");
let dimensions = (
node.attribute("width")
.map(LengthListParser::from)
.and_then(|mut parser| parser.next())
.transpose()
.expect("could not parse width")
.map(|width| length_to_mm(width, options.dpi)),
node.attribute("height")
.map(LengthListParser::from)
.and_then(|mut parser| parser.next())
.transpose()
.expect("could not parse height")
.map(|height| length_to_mm(height, options.dpi)),
);
let aspect_ratio = match (view_box, dimensions) {
(Some(ref view_box), (None, _)) | (Some(ref view_box), (_, None)) => {
view_box.w / view_box.h
}
(_, (Some(ref width), Some(ref height))) => *width / *height,
(None, (None, _)) | (None, (_, None)) => 1.,
};
if let Some(ref view_box) = view_box {
let view_box_transform = Transform2D::translation(-view_box.x, -view_box.y)
.then_scale(1. / view_box.w, 1. / view_box.h);
if node.has_tag_name(SVG_TAG_NAME) {
// Part 2 of converting from SVG to g-code coordinates
transforms.push(view_box_transform.then_translate(vector(0., -1.)));
} else {
transforms.push(view_box_transform);
}
}
if let Some(transform) = width_and_height_into_transform(&options, &node) {
transforms.push(transform);
let dimensions_override = [
options.dimensions[0].map(|dim_x| length_to_mm(dim_x, options.dpi)),
options.dimensions[1].map(|dim_y| length_to_mm(dim_y, options.dpi)),
];
match (dimensions_override, dimensions) {
([Some(dim_x), Some(dim_y)], _) if node.has_tag_name(SVG_TAG_NAME) => {
transforms.push(Transform2D::scale(dim_x, dim_y));
}
([Some(dim_x), None], _) if node.has_tag_name(SVG_TAG_NAME) => {
transforms.push(Transform2D::scale(dim_x, dim_x / aspect_ratio));
}
([None, Some(dim_y)], _) if node.has_tag_name(SVG_TAG_NAME) => {
transforms.push(Transform2D::scale(aspect_ratio * dim_y, dim_y));
}
(_, (Some(width), Some(height))) => {
transforms.push(Transform2D::scale(width, height));
}
(_, (Some(width), None)) => {
transforms.push(Transform2D::scale(width, width / aspect_ratio));
}
(_, (None, Some(height))) => {
transforms.push(Transform2D::scale(aspect_ratio * height, height));
}
(_, (None, None)) => {
if view_box.is_some() && node.has_tag_name(SVG_TAG_NAME) {
transforms.push(Transform2D::scale(aspect_ratio, 1.));
}
}
}
if let Some(transform) = node.attribute("transform") {
@ -166,31 +233,6 @@ fn node_name(node: &Node) -> String {
name
}
fn width_and_height_into_transform(
options: &ConversionOptions,
node: &Node,
) -> Option<Transform2D<f64>> {
if let (Some(mut width), Some(mut height)) = (
node.attribute("width").map(LengthListParser::from),
node.attribute("height").map(LengthListParser::from),
) {
let width = width
.next()
.expect("no width in width property")
.expect("cannot parse width");
let height = height
.next()
.expect("no height in height property")
.expect("cannot parse height");
let width_in_mm = length_to_mm(width, options.dpi);
let height_in_mm = length_to_mm(height, options.dpi);
Some(Transform2D::scale(width_in_mm, height_in_mm))
} else {
None
}
}
fn apply_path<'input>(
turtle: &'_ mut Turtle<'input>,
options: &ConversionOptions,

@ -21,9 +21,17 @@ pub use turtle::Turtle;
mod test {
use super::*;
use g_code::emit::{format_gcode_fmt, FormatOptions};
use svgtypes::{Length, LengthUnit};
fn get_actual(input: &str, circular_interpolation: bool) -> String {
let options = ConversionOptions::default();
fn get_actual(
input: &str,
circular_interpolation: bool,
dimensions: [Option<Length>; 2],
) -> String {
let options = ConversionOptions {
dimensions,
..Default::default()
};
let document = roxmltree::Document::parse(input).unwrap();
let mut turtle = Turtle::new(Machine::new(
@ -46,15 +54,41 @@ mod test {
#[test]
fn square_produces_expected_gcode() {
let square = include_str!("../tests/square.svg");
let actual = get_actual(square, false);
let actual = get_actual(square, false, [None; 2]);
assert_eq!(actual, include_str!("../tests/square.gcode"))
}
#[test]
fn square_dimension_override_produces_expected_gcode() {
let side_length = Length {
number: 10.,
unit: LengthUnit::Mm,
};
for square in [
include_str!("../tests/square.svg"),
include_str!("../tests/square_dimensionless.svg"),
] {
assert_eq!(
get_actual(square, false, [Some(side_length); 2]),
include_str!("../tests/square.gcode")
);
assert_eq!(
get_actual(square, false, [Some(side_length), None]),
include_str!("../tests/square.gcode")
);
assert_eq!(
get_actual(square, false, [None, Some(side_length)]),
include_str!("../tests/square.gcode")
);
}
}
#[test]
fn square_transformed_produces_expected_gcode() {
let square_transformed = include_str!("../tests/square_transformed.svg");
let actual = get_actual(square_transformed, false);
let actual = get_actual(square_transformed, false, [None; 2]);
assert_eq!(actual, include_str!("../tests/square_transformed.gcode"))
}
@ -62,7 +96,7 @@ mod test {
#[test]
fn square_viewport_produces_expected_gcode() {
let square_transformed = include_str!("../tests/square_viewport.svg");
let actual = get_actual(square_transformed, false);
let actual = get_actual(square_transformed, false, [None; 2]);
assert_eq!(actual, include_str!("../tests/square_viewport.gcode"))
}
@ -70,7 +104,7 @@ mod test {
#[test]
fn circular_interpolation_produces_expected_gcode() {
let circular_interpolation = include_str!("../tests/circular_interpolation.svg");
let actual = get_actual(circular_interpolation, true);
let actual = get_actual(circular_interpolation, true, [None; 2]);
assert_eq!(
actual,
@ -82,12 +116,12 @@ mod test {
fn svg_with_smooth_curves_produces_expected_gcode() {
let svg = include_str!("../tests/smooth_curves.svg");
assert_eq!(
get_actual(svg, false),
get_actual(svg, false, [None; 2]),
include_str!("../tests/smooth_curves.gcode")
);
assert_eq!(
get_actual(svg, true),
get_actual(svg, true, [None; 2]),
include_str!("../tests/smooth_curves_circular_interpolation.gcode")
);
}

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="square_transformed.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"
id="svg8"
version="1.1"
viewBox="0 0 10 10">
<defs
id="defs2" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="768"
inkscape:window-width="1366"
units="mm"
showgrid="true"
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:document-units="mm"
inkscape:cy="34.247607"
inkscape:cx="7.5651517"
inkscape:zoom="9.1957985"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base">
<inkscape:grid
spacingy="1"
spacingx="1"
units="mm"
id="grid836"
type="xygrid" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
id="path838"
d="M 1,1 H 9 V 9 H 1 Z"
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="path832"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.396875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
d="M 8,7.5 A 0.5,0.5 0 0 1 7.5,8 0.5,0.5 0 0 1 7,7.5 0.5,0.5 0 0 1 7.5,7 0.5,0.5 0 0 1 8,7.5 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -425,7 +425,7 @@ pub fn settings_form() -> Html {
/>
{" "}
<HyperlinkButton
ref={close_ref.clone()}
ref={close_ref}
style={ButtonStyle::Default}
title="Close"
href="#close"

@ -76,6 +76,7 @@ impl Component for App {
tolerance: app_state.tolerance,
feedrate: app_state.feedrate,
dpi: app_state.dpi,
dimensions: [None; 2],
};
let machine = Machine::new(
SupportedFunctionality {

Loading…
Cancel
Save