From 7019264735b3983837cc573be8d0e085ec0bceaf Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Sun, 31 Oct 2021 18:11:05 -0700 Subject: [PATCH] feat: support settings import/export, closes #22 --- Cargo.lock | 15 ++- cli/Cargo.toml | 5 +- cli/src/main.rs | 202 ++++++++++++++++++++++++------------- lib/Cargo.toml | 11 ++- lib/src/converter.rs | 219 +++++++++++++++++++++++++++++++++++------ lib/src/lib.rs | 26 +++-- lib/src/machine.rs | 31 ++++-- lib/src/postprocess.rs | 9 ++ web/Cargo.toml | 5 +- web/src/inputs.rs | 188 ++++++++++++++++++++++++++++------- web/src/main.rs | 66 +++++++------ web/src/spectre/mod.rs | 63 ++++++------ web/src/state.rs | 102 +++++++++++++------ web/src/util.rs | 17 ++++ web/style/main.scss | 9 ++ 15 files changed, 708 insertions(+), 260 deletions(-) create mode 100644 web/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index e3f15de..c36f308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,15 +228,16 @@ checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" [[package]] name = "g-code" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2403e3a8d80208fa702f2bc4588d8ea28ed456c7e552891eea6e7e9c68582167" +checksum = "8220033ab3971720c5ec54c4b7d36b35b87e3e58d53785152ffbdfd72d8e4c80" dependencies = [ "codespan", "codespan-reporting", "paste", "peg", "rust_decimal", + "serde", ] [[package]] @@ -632,7 +633,7 @@ dependencies = [ [[package]] name = "svg2gcode" -version = "0.0.5" +version = "0.0.6" dependencies = [ "cairo-rs", "euclid", @@ -641,19 +642,22 @@ dependencies = [ "lyon_geom", "paste", "roxmltree", + "serde", + "serde_json", "svgtypes", "uom", ] [[package]] name = "svg2gcode-cli" -version = "0.0.1" +version = "0.0.2" dependencies = [ "codespan-reporting", "env_logger", "g-code", "log", "roxmltree", + "serde_json", "structopt", "svg2gcode", "svgtypes", @@ -661,7 +665,7 @@ dependencies = [ [[package]] name = "svg2gcode-web" -version = "0.0.1" +version = "0.0.2" dependencies = [ "base64", "codespan", @@ -674,6 +678,7 @@ dependencies = [ "paste", "roxmltree", "serde", + "serde_json", "svg2gcode", "svgtypes", "wasm-bindgen", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4ec0002..b981739 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "svg2gcode-cli" -version = "0.0.1" +version = "0.0.2" authors = ["Sameer Puri "] edition = "2018" description = "Command line interface for svg2gcode" @@ -8,7 +8,7 @@ repository = "https://github.com/sameer/svg2gcode" license = "MIT" [dependencies] -svg2gcode = { path = "../lib" } +svg2gcode = { path = "../lib", features = ["serde"]} env_logger = { version = "0", default-features = false, features = ["atty", "termcolor", "humantime"] } log = "0" g-code = "0" @@ -16,6 +16,7 @@ codespan-reporting = "0.11" structopt = "0.3" roxmltree = "0" svgtypes = "0" +serde_json = "1" [[bin]] name = "svg2gcode" diff --git a/cli/src/main.rs b/cli/src/main.rs index 2dfee20..f42291e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,29 +6,29 @@ use log::info; use std::{ env, fs::File, - io::{self, Read}, + io::{self, Read, Write}, path::PathBuf, }; use structopt::StructOpt; use svgtypes::LengthListParser; use svg2gcode::{ - set_origin, svg2program, ConversionOptions, Machine, SupportedFunctionality, Turtle, + set_origin, svg2program, ConversionOptions, Machine, Settings, SupportedFunctionality, Turtle, }; #[derive(Debug, StructOpt)] #[structopt(name = "svg2gcode", author, about)] struct Opt { /// Curve interpolation tolerance (mm) - #[structopt(long, default_value = "0.002")] - tolerance: f64, + #[structopt(long)] + tolerance: Option, /// Machine feed rate (mm/min) - #[structopt(long, default_value = "300")] - feedrate: f64, + #[structopt(long)] + feedrate: Option, /// Dots per Inch (DPI) /// Used for scaling visual units (pixels, points, picas, etc.) - #[structopt(long, default_value = "96")] - dpi: f64, + #[structopt(long)] + dpi: Option, #[structopt(alias = "tool_on_sequence", long = "on")] /// G-Code for turning on the tool tool_on_sequence: Option, @@ -46,9 +46,17 @@ struct Opt { /// Output file path (overwrites old files), else writes to stdout #[structopt(short, long)] out: Option, + /// Provide settings from a JSON file. Overrides command-line arguments. + #[structopt(long)] + settings: Option, + /// Export current settings to a JSON file instead of converting. + /// + /// Use `-` to export to standard out. + #[structopt(long)] + export: Option, /// Coordinates for the bottom left corner of the machine - #[structopt(long, default_value = "0,0")] - origin: String, + #[structopt(long)] + origin: Option, /// 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) @@ -60,7 +68,7 @@ struct Opt { /// /// Please check if your machine supports G2/G3 commands before enabling this. #[structopt(long)] - circular_interpolation: bool, + circular_interpolation: Option, } fn main() -> io::Result<()> { @@ -71,6 +79,95 @@ fn main() -> io::Result<()> { let opt = Opt::from_args(); + let settings = { + let mut settings = if let Some(path) = opt.settings { + serde_json::from_reader(File::open(path)?)? + } else { + Settings::default() + }; + + { + let conversion = &mut settings.conversion; + conversion.dpi = opt.dpi.unwrap_or(conversion.dpi); + conversion.feedrate = opt.feedrate.unwrap_or(conversion.feedrate); + conversion.tolerance = opt.dpi.unwrap_or(conversion.tolerance); + } + { + let machine = &mut settings.machine; + machine.supported_functionality = SupportedFunctionality { + circular_interpolation: opt + .circular_interpolation + .unwrap_or(machine.supported_functionality.circular_interpolation), + }; + if let Some(sequence) = opt.tool_on_sequence { + machine.tool_on_sequence.insert(sequence); + } + if let Some(sequence) = opt.tool_off_sequence { + machine.tool_off_sequence.insert(sequence); + } + if let Some(sequence) = opt.begin_sequence { + machine.begin_sequence.insert(sequence); + } + if let Some(sequence) = opt.end_sequence { + machine.end_sequence.insert(sequence); + } + } + { + let postprocess = &mut settings.postprocess; + if let Some(origin) = opt.origin { + for (i, dimension_origin) in origin + .split(',') + .map(|point| { + if point.is_empty() { + Default::default() + } else { + point.parse().expect("could not parse coordinate") + } + }) + .take(2) + .enumerate() + { + postprocess.origin[i] = dimension_origin; + } + } + } + settings + }; + + if let Some(export_path) = opt.export { + let mut config_json_bytes = serde_json::to_vec_pretty(&settings)?; + if export_path.to_string_lossy() == "-" { + return io::stdout().write_all(&mut config_json_bytes); + } else { + return File::create(export_path)?.write_all(&mut config_json_bytes); + } + } + + let options = { + 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; + }); + } + ConversionOptions { dimensions } + }; + let input = match opt.file { Some(filename) => { let mut f = File::open(filename)?; @@ -87,58 +184,38 @@ 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 = [ - opt.tool_on_sequence + settings + .machine + .tool_on_sequence + .as_deref() + .map(snippet_parser) + .transpose(), + settings + .machine + .tool_off_sequence .as_deref() .map(snippet_parser) .transpose(), - opt.tool_off_sequence + settings + .machine + .begin_sequence .as_deref() .map(snippet_parser) .transpose(), - opt.begin_sequence + settings + .machine + .end_sequence .as_deref() .map(snippet_parser) .transpose(), - opt.end_sequence.as_deref().map(snippet_parser).transpose(), ]; let machine = if let [Ok(tool_on_action), Ok(tool_off_action), Ok(program_begin_sequence), Ok(program_end_sequence)] = snippets { Machine::new( - SupportedFunctionality { - circular_interpolation: opt.circular_interpolation, - }, + settings.machine.supported_functionality, tool_on_action, tool_off_action, program_begin_sequence, @@ -153,10 +230,10 @@ fn main() -> io::Result<()> { let config = codespan_reporting::term::Config::default(); for (i, (filename, gcode)) in [ - ("tool_on_sequence", &opt.tool_on_sequence), - ("tool_off_sequence", &opt.tool_off_sequence), - ("begin_sequence", &opt.begin_sequence), - ("end_sequence", &opt.end_sequence), + ("tool_on_sequence", &settings.machine.tool_on_sequence), + ("tool_off_sequence", &settings.machine.tool_off_sequence), + ("begin_sequence", &settings.machine.begin_sequence), + ("end_sequence", &settings.machine.end_sequence), ] .iter() .enumerate() @@ -177,26 +254,9 @@ fn main() -> io::Result<()> { let document = roxmltree::Document::parse(&input).unwrap(); let mut turtle = Turtle::new(machine); - let mut program = svg2program(&document, options, &mut turtle); - - let mut origin = [0., 0.]; - - for (i, dimension_origin) in opt - .origin - .split(',') - .map(|point| { - if point.is_empty() { - Default::default() - } else { - point.parse().expect("could not parse coordinate") - } - }) - .take(2) - .enumerate() - { - origin[i] = dimension_origin; - } - set_origin(&mut program, origin); + let mut program = svg2program(&document, &settings.conversion, options, &mut turtle); + + set_origin(&mut program, settings.postprocess.origin); if let Some(out_path) = opt.out { format_gcode_io(&program, FormatOptions::default(), File::create(out_path)?) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 46f8b05..fe17654 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "svg2gcode" -version = "0.0.5" +version = "0.0.6" authors = ["Sameer Puri "] edition = "2018" description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine." @@ -8,7 +8,7 @@ repository = "https://github.com/sameer/svg2gcode" license = "MIT" [dependencies] -g-code = "0.3.1" +g-code = { version = "0.3.3", features = ["serde"] } lyon_geom = ">= 0.17.5" euclid = "0.22" log = "0.4" @@ -17,5 +17,12 @@ roxmltree = "0.14" svgtypes = "0.6" paste = "1.0" +[dependencies.serde] +default-features = false +optional = true +version = "1" +features = ["derive"] + [dev-dependencies] cairo-rs = { version = "0.14", default-features = false, features = ["svg", "v1_16"] } +serde_json = "1" diff --git a/lib/src/converter.rs b/lib/src/converter.rs index e1950ca..afa5985 100644 --- a/lib/src/converter.rs +++ b/lib/src/converter.rs @@ -8,43 +8,136 @@ use lyon_geom::{ point, vector, ArcFlags, }; use roxmltree::{Document, Node}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use svgtypes::{ - Length, LengthListParser, LengthUnit, 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 { +/// High-level output configuration +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ConversionConfig { /// Curve interpolation tolerance in millimeters pub tolerance: f64, /// Feedrate in millimeters / minute 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; 2], } -impl Default for ConversionOptions { +impl Default for ConversionConfig { fn default() -> Self { Self { tolerance: 0.002, feedrate: 300.0, dpi: 96.0, - dimensions: [None; 2], } } } +/// Options are specific to this conversion. +/// +/// This is separate from [ConversionConfig] to support bulk processing in the web interface. +#[derive(Debug, Clone, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ConversionOptions { + /// Width and height override + /// + /// Useful when an SVG does not have a set width and height or you want to override it. + #[cfg_attr(feature = "serde", serde(with = "length_serde"))] + pub dimensions: [Option; 2], +} + +#[cfg(feature = "serde")] +mod length_serde { + use serde::{ + de::{SeqAccess, Visitor}, + ser::SerializeSeq, + Deserialize, Deserializer, Serialize, Serializer, + }; + use svgtypes::{Length, LengthUnit}; + + pub fn serialize(length: &[Option; 2], serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(2))?; + for i in 0..2 { + let length_def = length[i].clone().map(|length| LengthDef { + number: length.number, + unit: length.unit, + }); + seq.serialize_element(&length_def)?; + } + seq.end() + } + + struct OptionalLengthArrayVisitor; + impl<'de> Visitor<'de> for OptionalLengthArrayVisitor { + type Value = [Option; 2]; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "SVG dimension array") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let x = seq.next_element::>()?.flatten(); + let y = seq.next_element::>()?.flatten(); + Ok([ + x.map(|length_def| Length { + number: length_def.number, + unit: length_def.unit, + }), + y.map(|length_def| Length { + number: length_def.number, + unit: length_def.unit, + }), + ]) + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<[Option; 2], D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(OptionalLengthArrayVisitor) + } + + #[derive(Serialize, Deserialize)] + struct LengthDef { + number: f64, + #[serde(with = "LengthUnitDef")] + unit: LengthUnit, + } + + #[derive(Serialize, Deserialize)] + #[serde(remote = "LengthUnit")] + enum LengthUnitDef { + None, + Em, + Ex, + Px, + In, + Cm, + Mm, + Pt, + Pc, + Percent, + } +} + pub fn svg2program<'input>( doc: &Document, + config: &ConversionConfig, options: ConversionOptions, turtle: &'input mut Turtle<'input>, ) -> Vec> { @@ -110,13 +203,13 @@ pub fn svg2program<'input>( .and_then(|mut parser| parser.next()) .transpose() .expect("could not parse width") - .map(|width| length_to_mm(width, options.dpi, scale_w)), + .map(|width| length_to_mm(width, config.dpi, scale_w)), 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, scale_h)), + .map(|height| length_to_mm(height, config.dpi, scale_h)), ); let aspect_ratio = match (view_box, dimensions) { (_, (Some(ref width), Some(ref height))) => *width / *height, @@ -136,8 +229,8 @@ pub fn svg2program<'input>( } let dimensions_override = [ - options.dimensions[0].map(|dim_x| length_to_mm(dim_x, options.dpi, scale_w)), - options.dimensions[1].map(|dim_y| length_to_mm(dim_y, options.dpi, scale_h)), + options.dimensions[0].map(|dim_x| length_to_mm(dim_x, config.dpi, scale_w)), + options.dimensions[1].map(|dim_y| length_to_mm(dim_y, config.dpi, scale_h)), ]; match (dimensions_override, dimensions) { @@ -203,7 +296,7 @@ pub fn svg2program<'input>( is_inline: false, inner: Cow::Owned(comment), }); - program.extend(apply_path(turtle, &options, d)); + program.extend(apply_path(turtle, config, d)); } else { warn!("There is a path node containing no actual path: {:?}", node); } @@ -238,7 +331,7 @@ fn node_name(node: &Node) -> String { fn apply_path<'input>( turtle: &'_ mut Turtle<'input>, - options: &ConversionOptions, + config: &ConversionConfig, path: &str, ) -> Vec> { use PathSegment::*; @@ -250,11 +343,11 @@ fn apply_path<'input>( MoveTo { abs, x, y } => turtle.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(options.feedrate) + turtle.close(config.feedrate) } - LineTo { abs, x, y } => turtle.line(abs, x, y, options.feedrate), - HorizontalLineTo { abs, x } => turtle.line(abs, x, None, options.feedrate), - VerticalLineTo { abs, y } => turtle.line(abs, None, y, options.feedrate), + LineTo { abs, x, y } => turtle.line(abs, x, y, config.feedrate), + HorizontalLineTo { abs, x } => turtle.line(abs, x, None, config.feedrate), + VerticalLineTo { abs, y } => turtle.line(abs, None, y, config.feedrate), CurveTo { abs, x1, @@ -268,28 +361,28 @@ fn apply_path<'input>( point(x1, y1), point(x2, y2), point(x, y), - options.tolerance, - options.feedrate, + config.tolerance, + config.feedrate, ), SmoothCurveTo { abs, x2, y2, x, y } => turtle.smooth_cubic_bezier( abs, point(x2, y2), point(x, y), - options.tolerance, - options.feedrate, + config.tolerance, + config.feedrate, ), Quadratic { abs, x1, y1, x, y } => turtle.quadratic_bezier( abs, point(x1, y1), point(x, y), - options.tolerance, - options.feedrate, + config.tolerance, + config.feedrate, ), SmoothQuadratic { abs, x, y } => turtle.smooth_quadratic_bezier( abs, point(x, y), - options.tolerance, - options.feedrate, + config.tolerance, + config.feedrate, ), EllipticalArc { abs, @@ -306,8 +399,8 @@ fn apply_path<'input>( Angle::degrees(x_axis_rotation), ArcFlags { large_arc, sweep }, point(x, y), - options.feedrate, - options.tolerance, + config.feedrate, + config.tolerance, ), } }) @@ -373,3 +466,67 @@ fn length_to_mm(l: svgtypes::Length, dpi: f64, scale: Option) -> f64 { length.get::() } + +#[cfg(test)] +mod test { + use super::*; + #[cfg(feature = "serde")] + use svgtypes::LengthUnit; + + #[test] + #[cfg(feature = "serde")] + fn serde_conversion_options_is_correct() { + let default_struct = ConversionOptions::default(); + let default_json = "{\"dimensions\":[null,null]}"; + + assert_eq!( + serde_json::to_string(&default_struct).unwrap(), + default_json + ); + assert_eq!( + serde_json::from_str::(default_json).unwrap(), + default_struct + ); + } + + #[test] + #[cfg(feature = "serde")] + fn serde_conversion_options_with_single_dimension_is_correct() { + let mut r#struct = ConversionOptions::default(); + r#struct.dimensions[0] = Some(Length { + number: 4., + unit: LengthUnit::Mm, + }); + let json = "{\"dimensions\":[{\"number\":4.0,\"unit\":\"Mm\"},null]}"; + + assert_eq!(serde_json::to_string(&r#struct).unwrap(), json); + assert_eq!( + serde_json::from_str::(json).unwrap(), + r#struct + ); + } + + #[test] + #[cfg(feature = "serde")] + fn serde_conversion_options_with_both_dimensions_is_correct() { + let mut r#struct = ConversionOptions::default(); + r#struct.dimensions = [ + Some(Length { + number: 4., + unit: LengthUnit::Mm, + }), + Some(Length { + number: 10.5, + unit: LengthUnit::In, + }), + ]; + let json = + "{\"dimensions\":[{\"number\":4.0,\"unit\":\"Mm\"},{\"number\":10.5,\"unit\":\"In\"}]}"; + + assert_eq!(serde_json::to_string(&r#struct).unwrap(), json); + assert_eq!( + serde_json::from_str::(json).unwrap(), + r#struct + ); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 24860cc..780b4b4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + /// Approximate [Bézier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) with [Circular arcs](https://en.wikipedia.org/wiki/Circular_arc) mod arc; /// Converts an SVG to G-Code in an internal representation @@ -11,12 +14,19 @@ mod postprocess; /// This concept is referred to as [Turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics). mod turtle; -pub use converter::{svg2program, ConversionOptions}; -pub use machine::Machine; -pub use machine::SupportedFunctionality; -pub use postprocess::set_origin; +pub use converter::{svg2program, ConversionConfig, ConversionOptions}; +pub use machine::{Machine, MachineConfig, SupportedFunctionality}; +pub use postprocess::{set_origin, PostprocessConfig}; pub use turtle::Turtle; +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Settings { + pub conversion: ConversionConfig, + pub machine: MachineConfig, + pub postprocess: PostprocessConfig, +} + #[cfg(test)] mod test { use super::*; @@ -28,10 +38,8 @@ mod test { circular_interpolation: bool, dimensions: [Option; 2], ) -> String { - let options = ConversionOptions { - dimensions, - ..Default::default() - }; + let config = ConversionConfig::default(); + let options = ConversionOptions { dimensions }; let document = roxmltree::Document::parse(input).unwrap(); let mut turtle = Turtle::new(Machine::new( @@ -43,7 +51,7 @@ mod test { None, None, )); - let mut program = converter::svg2program(&document, options, &mut turtle); + let mut program = converter::svg2program(&document, &config, options, &mut turtle); postprocess::set_origin(&mut program, [0., 0.]); let mut acc = String::new(); diff --git a/lib/src/machine.rs b/lib/src/machine.rs index 6f8b84a..e2e805d 100644 --- a/lib/src/machine.rs +++ b/lib/src/machine.rs @@ -1,4 +1,6 @@ use g_code::{command, emit::Token, parse::ast::Snippet}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; /// Whether the tool is active (i.e. cutting) #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -41,13 +43,24 @@ pub struct Machine<'input> { supported_functionality: SupportedFunctionality, tool_state: Option, distance_mode: Option, - tool_on_action: Vec>, - tool_off_action: Vec>, + tool_on_sequence: Vec>, + tool_off_sequence: Vec>, program_begin_sequence: Vec>, program_end_sequence: Vec>, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MachineConfig { + pub supported_functionality: SupportedFunctionality, + pub tool_on_sequence: Option, + pub tool_off_sequence: Option, + pub begin_sequence: Option, + pub end_sequence: Option, +} + +#[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SupportedFunctionality { /// Indicates support for G2/G3 circular interpolation. /// @@ -58,17 +71,17 @@ pub struct SupportedFunctionality { impl<'input> Machine<'input> { pub fn new( supported_functionality: SupportedFunctionality, - tool_on_action: Option>, - tool_off_action: Option>, + tool_on_sequence: Option>, + tool_off_sequence: Option>, program_begin_sequence: Option>, program_end_sequence: Option>, ) -> Self { Self { supported_functionality, - tool_on_action: tool_on_action + tool_on_sequence: tool_on_sequence .map(|s| s.iter_emit_tokens().collect()) .unwrap_or_default(), - tool_off_action: tool_off_action + tool_off_sequence: tool_off_sequence .map(|s| s.iter_emit_tokens().collect()) .unwrap_or_default(), program_begin_sequence: program_begin_sequence @@ -89,7 +102,7 @@ impl<'input> Machine<'input> { pub fn tool_on(&mut self) -> Vec> { if self.tool_state == Some(Tool::Off) || self.tool_state == None { self.tool_state = Some(Tool::On); - self.tool_on_action.clone() + self.tool_on_sequence.clone() } else { vec![] } @@ -99,7 +112,7 @@ impl<'input> Machine<'input> { pub fn tool_off(&mut self) -> Vec> { if self.tool_state == Some(Tool::On) || self.tool_state == None { self.tool_state = Some(Tool::Off); - self.tool_off_action.clone() + self.tool_off_sequence.clone() } else { vec![] } diff --git a/lib/src/postprocess.rs b/lib/src/postprocess.rs index 3bcfd6a..418868f 100644 --- a/lib/src/postprocess.rs +++ b/lib/src/postprocess.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + use euclid::default::Box2D; use g_code::emit::{ command::{ABSOLUTE_DISTANCE_MODE_FIELD, RELATIVE_DISTANCE_MODE_FIELD}, @@ -7,6 +10,12 @@ use lyon_geom::{point, vector, Point}; type F64Point = Point; +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Default, Clone, PartialEq)] +pub struct PostprocessConfig { + pub origin: [f64; 2], +} + /// Moves all the commands so that they are beyond a specified position pub fn set_origin(tokens: &mut [Token<'_>], origin: [f64; 2]) { let offset = diff --git a/web/Cargo.toml b/web/Cargo.toml index 82809c1..4a8ff8f 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "svg2gcode-web" -version = "0.0.1" +version = "0.0.2" authors = ["Sameer Puri "] edition = "2018" description = "Convert vector graphics to g-code for pen plotters, laser engravers, and other CNC machines" @@ -10,7 +10,7 @@ license = "MIT" [dependencies] wasm-bindgen = "0.2" -svg2gcode = { path = "../lib" } +svg2gcode = { path = "../lib", features = ["serde"] } roxmltree = "0" g-code = "0" codespan-reporting = "0.11" @@ -19,6 +19,7 @@ serde = "1" paste = "1" log = "0" svgtypes = "0" +serde_json = "1" yew = { git = "https://github.com/yewstack/yew.git" } yewdux-functional = { git = "https://github.com/intendednull/yewdux.git" } diff --git a/web/src/inputs.rs b/web/src/inputs.rs index ae9b392..48aa43a 100644 --- a/web/src/inputs.rs +++ b/web/src/inputs.rs @@ -1,11 +1,12 @@ use codespan_reporting::term::{emit, termcolor::NoColor, Config}; use g_code::parse::{into_diagnostic, snippet_parser}; -use gloo_file::futures::read_as_text; +use gloo_file::futures::{read_as_bytes, read_as_text}; use gloo_timers::callback::Timeout; use js_sys::TypeError; use paste::paste; use roxmltree::Document; -use std::num::ParseFloatError; +use std::{convert::TryInto, num::ParseFloatError, path::Path}; +use svg2gcode::Settings; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{window, FileList, HtmlElement, Response}; @@ -171,7 +172,8 @@ macro_rules! gcode_input { ($($name: ident { $label: literal, $desc: literal, - $accessor: expr $(=> $idx: literal)?, + $form_accessor: expr $(=> $form_idx: literal)?, + $app_accessor: expr $(=> $app_idx: literal)?, })*) => { $( paste! { @@ -207,14 +209,14 @@ macro_rules! gcode_input { timeout.set(Some(Timeout::new(VALIDATION_TIMEOUT, move || { timeout_inner.set(None); }))); - state.$accessor $([$idx])? = res; + state.$form_accessor $([$form_idx])? = res; }) }; html! { - + label=$label desc=$desc - default={app.state().map(|state| (state.$accessor $([$idx])?).clone()).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)} - parsed={form.state().and_then(|state| (state.$accessor $([$idx])?).clone()).filter(|_| timeout.is_none())} + default={app.state().map(|state| (state.$app_accessor $([$app_idx])?).clone()).unwrap_or_else(|| AppState::default().$app_accessor $([$app_idx])?)} + parsed={form.state().and_then(|state| (state.$form_accessor $([$form_idx])?).clone()).filter(|_| timeout.is_none())} oninput={oninput} /> @@ -230,21 +232,25 @@ gcode_input! { "Tool On Sequence", "G-Code for turning on the tool", tool_on_sequence, + settings.machine.tool_on_sequence, } ToolOffSequence { "Tool Off Sequence", "G-Code for turning off the tool", tool_off_sequence, + settings.machine.tool_off_sequence, } BeginSequence { "Program Begin Sequence", "G-Code for initializing the machine at the beginning of the program", begin_sequence, + settings.machine.begin_sequence, } EndSequence { "Program End Sequence", "G-Code for stopping/idling the machine at the end of the program", end_sequence, + settings.machine.end_sequence, } } @@ -252,7 +258,8 @@ macro_rules! form_input { ($($name: ident { $label: literal, $desc: literal, - $accessor: expr $(=> $idx: literal)?, + $form_accessor: expr $(=> $form_idx: literal)?, + $app_accessor: expr $(=> $app_idx: literal)?, })*) => { $( paste! { @@ -260,12 +267,12 @@ macro_rules! form_input { fn [<$name:snake:lower _input>]() -> Html { let app = use_store::(); let form = use_store::>(); - let oninput = form.dispatch().input(|state, value| state.$accessor $([$idx])? = value.parse::()); + let oninput = form.dispatch().input(|state, value| state.$form_accessor $([$form_idx])? = value.parse::()); html! { - + label=$label desc=$desc - default={app.state().map(|state| state.$accessor $([$idx])?).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)} - parsed={form.state().map(|state| (state.$accessor $([$idx])?).clone())} + default={app.state().map(|state| state.$app_accessor $([$app_idx])?).unwrap_or_else(|| AppState::default().$app_accessor $([$app_idx])?)} + parsed={form.state().map(|state| (state.$form_accessor $([$form_idx])?).clone())} oninput={oninput} /> @@ -281,26 +288,31 @@ form_input! { "Tolerance", "Curve interpolation tolerance (mm)", tolerance, + settings.conversion.tolerance, } Feedrate { "Feedrate", "Machine feedrate (mm/min)", feedrate, + settings.conversion.feedrate, } Dpi { "Dots per Inch", "Used for scaling visual units (pixels, points, picas, etc.)", dpi, + settings.conversion.dpi, } OriginX { "Origin X", "X-axis coordinate for the bottom left corner of the machine", origin => 0, + settings.postprocess.origin => 0, } OriginY { "Origin Y", "Y-axis coordinate for the bottom left corner of the machine", origin => 1, + settings.postprocess.origin => 1, } } @@ -357,18 +369,7 @@ pub fn settings_form() -> Html { let close_ref = close_ref.clone(); app.dispatch().reduce_callback(move |app| { if let (false, Some(form)) = (disabled, form.state()) { - app.tolerance = *form.tolerance.as_ref().unwrap(); - app.feedrate = *form.feedrate.as_ref().unwrap(); - app.origin = [ - *form.origin[0].as_ref().unwrap(), - *form.origin[1].as_ref().unwrap(), - ]; - app.circular_interpolation = form.circular_interpolation; - app.dpi = *form.dpi.as_ref().unwrap(); - app.tool_on_sequence = form.tool_on_sequence.clone().and_then(Result::ok); - app.tool_off_sequence = form.tool_off_sequence.clone().and_then(Result::ok); - app.begin_sequence = form.begin_sequence.clone().and_then(Result::ok); - app.end_sequence = form.end_sequence.clone().and_then(Result::ok); + app.settings = form.as_ref().try_into().unwrap(); // TODO: this is a poor man's crutch for closing the Modal. // There is probably a better way. @@ -384,7 +385,17 @@ pub fn settings_form() -> Html { id="settings" header={ html!( -
{ "Settings" }
+ <> +

{ "Settings" }

+

{"Persisted using "} + // Opening new tabs is usually bad. + // But if we don't, the user is at risk of losing the settings they've entered so far. + + {"local storage"} + + {"."} +

+ ) } body={html!( @@ -411,15 +422,6 @@ pub fn settings_form() -> Html { footer={ html!( <> -

- {"These settings are persisted using local storage. Learn more "} - // Opening new tabs is usually bad. - // But if we don't, the user is at risk of losing the settings they've entered so far. - - {"on MDN"} - - {"."} -