update dependencies, fix #36

master
Sameer Puri 2 years ago
parent 2322bc14bc
commit 99cde4ae4b

1408
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,8 +1,8 @@
[package]
name = "svg2gcode-cli"
version = "0.0.2"
version = "0.0.4"
authors = ["Sameer Puri <crates@purisa.me>"]
edition = "2018"
edition = "2021"
description = "Command line interface for svg2gcode"
repository = "https://github.com/sameer/svg2gcode"
license = "MIT"
@ -11,11 +11,11 @@ license = "MIT"
svg2gcode = { path = "../lib", features = ["serde"]}
env_logger = { version = "0", default-features = false, features = ["atty", "termcolor", "humantime"] }
log = "0"
g-code = "0"
g-code = "0.3"
codespan-reporting = "0.11"
structopt = "0.3"
roxmltree = "0"
svgtypes = "0"
roxmltree = "0.18"
svgtypes = "0.11"
serde_json = "1"
[[bin]]

@ -97,17 +97,17 @@ fn main() -> io::Result<()> {
.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 seq @ Some(_) = opt.tool_on_sequence {
machine.tool_on_sequence = seq;
}
if let Some(sequence) = opt.tool_off_sequence {
machine.tool_off_sequence.insert(sequence);
if let seq @ Some(_) = opt.tool_off_sequence {
machine.tool_off_sequence = seq;
}
if let Some(sequence) = opt.begin_sequence {
machine.begin_sequence.insert(sequence);
if let seq @ Some(_) = opt.begin_sequence {
machine.begin_sequence = seq;
}
if let Some(sequence) = opt.end_sequence {
machine.end_sequence.insert(sequence);
if let seq @ Some(_) = opt.end_sequence {
machine.end_sequence = seq;
}
}
{
@ -240,7 +240,7 @@ fn main() -> io::Result<()> {
&mut writer,
&config,
&codespan_reporting::files::SimpleFile::new(filename, gcode.as_ref().unwrap()),
&g_code::parse::into_diagnostic(&err),
&g_code::parse::into_diagnostic(err),
)
.unwrap();
}

@ -1,20 +1,20 @@
[package]
name = "svg2gcode"
version = "0.0.7"
version = "0.1.1"
authors = ["Sameer Puri <crates@purisa.me>"]
edition = "2018"
edition = "2021"
description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine."
repository = "https://github.com/sameer/svg2gcode"
license = "MIT"
[dependencies]
g-code = { version = "0.3.3", features = ["serde"] }
lyon_geom = ">= 0.17.5"
g-code = { version = "0.3.4", features = ["serde"] }
lyon_geom = "1.0.4"
euclid = "0.22"
log = "0.4"
uom = "0.31.1"
roxmltree = "0.14"
svgtypes = "0.6"
uom = "0.34.0"
roxmltree = "0.18"
svgtypes = "0.11"
paste = "1.0"
[dependencies.serde]
@ -24,5 +24,5 @@ version = "1"
features = ["derive"]
[dev-dependencies]
cairo-rs = { version = "0.14", default-features = false, features = ["svg", "v1_16"] }
cairo-rs = { version = "0.17", default-features = false, features = ["svg", "v1_16"] }
serde_json = "1"

@ -99,7 +99,7 @@ where
}
let mut acc = vec![];
self.for_each_monotonic_range(|range| {
self.for_each_monotonic_range(&mut |range| {
let inner_bezier = self.split_range(range);
if (inner_bezier.to - inner_bezier.from).square_length() < S::EPSILON {

@ -50,7 +50,7 @@ impl Default for ConversionConfig {
tolerance: 0.002,
feedrate: 300.0,
dpi: 96.0,
origin: [Some(0.); 2],
origin: zero_origin(),
}
}
}

@ -16,7 +16,7 @@ mod turtle;
pub use converter::{svg2program, ConversionConfig, ConversionOptions};
pub use machine::{Machine, MachineConfig, SupportedFunctionality};
pub use postprocess::{set_origin, PostprocessConfig};
pub use postprocess::PostprocessConfig;
pub use turtle::Turtle;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

@ -80,7 +80,7 @@ impl<'input> Machine<'input> {
/// Output gcode to turn the tool on.
pub fn tool_on(&mut self) -> Vec<Token<'input>> {
if self.tool_state == Some(Tool::Off) || self.tool_state == None {
if self.tool_state == Some(Tool::Off) || self.tool_state.is_none() {
self.tool_state = Some(Tool::On);
self.tool_on_sequence.clone()
} else {
@ -90,7 +90,7 @@ impl<'input> Machine<'input> {
/// Output gcode to turn the tool off.
pub fn tool_off(&mut self) -> Vec<Token<'input>> {
if self.tool_state == Some(Tool::On) || self.tool_state == None {
if self.tool_state == Some(Tool::On) || self.tool_state.is_none() {
self.tool_state = Some(Tool::Off);
self.tool_off_sequence.clone()
} else {
@ -110,7 +110,7 @@ impl<'input> Machine<'input> {
/// Output absolute distance field if mode was relative or unknown.
pub fn absolute(&mut self) -> Vec<Token<'input>> {
if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None {
if self.distance_mode == Some(Distance::Relative) || self.distance_mode.is_none() {
self.distance_mode = Some(Distance::Absolute);
command!(AbsoluteDistanceMode {}).into_token_vec()
} else {
@ -120,7 +120,7 @@ impl<'input> Machine<'input> {
/// Output relative distance field if mode was absolute or unknown.
pub fn relative(&mut self) -> Vec<Token<'input>> {
if self.distance_mode == Some(Distance::Absolute) || self.distance_mode == None {
if self.distance_mode == Some(Distance::Absolute) || self.distance_mode.is_none() {
self.distance_mode = Some(Distance::Relative);
command!(RelativeDistanceMode {}).into_token_vec()
} else {

@ -1,105 +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},
Field, Token, Value,
};
use lyon_geom::{point, vector, Point};
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();
let mut is_relative = false;
let mut current_position = point(0f64, 0f64);
let mut should_skip = false;
for token in tokens {
match token {
abs if *abs == Token::Field(ABSOLUTE_DISTANCE_MODE_FIELD) => is_relative = false,
rel if *rel == Token::Field(RELATIVE_DISTANCE_MODE_FIELD) => is_relative = true,
// Don't edit M codes for relativity
Token::Field(Field { letters, .. }) if *letters == "M" => should_skip = true,
Token::Field(Field { letters, .. }) if *letters == "G" => should_skip = false,
Token::Field(Field { letters, value }) if *letters == "X" && !should_skip => {
if let Some(float) = value.as_f64() {
if is_relative {
current_position += vector(float, 0.)
} else {
current_position = point(float, 0.);
}
*value = Value::Float(current_position.x + offset.x)
}
}
Token::Field(Field { letters, value }) if *letters == "Y" && !should_skip => {
if let Some(float) = value.as_f64() {
if is_relative {
current_position += vector(0., float)
} else {
current_position = point(0., float);
}
*value = Value::Float(current_position.y + offset.y)
}
}
_ => {}
}
}
}
fn get_bounding_box<'a, I: Iterator<Item = &'a Token<'a>>>(tokens: I) -> Box2D<f64> {
let (mut minimum, mut maximum) = (point(0f64, 0f64), point(0f64, 0f64));
let mut is_relative = false;
let mut should_skip = false;
let mut current_position = point(0f64, 0f64);
for token in tokens {
match token {
abs if *abs == Token::Field(ABSOLUTE_DISTANCE_MODE_FIELD) => is_relative = false,
rel if *rel == Token::Field(RELATIVE_DISTANCE_MODE_FIELD) => is_relative = true,
// Don't check M codes for relativity
Token::Field(Field { letters, .. }) if *letters == "M" => should_skip = true,
Token::Field(Field { letters, .. }) if *letters == "G" => should_skip = false,
Token::Field(Field { letters, value }) if *letters == "X" && !should_skip => {
if let Some(value) = value.as_f64() {
if is_relative {
current_position += vector(value, 0.)
} else {
current_position = point(value, 0.);
}
minimum = minimum.min(current_position);
maximum = maximum.max(current_position);
}
}
Token::Field(Field { letters, value }) if *letters == "Y" && !should_skip => {
if let Some(value) = value.as_f64() {
if is_relative {
current_position += vector(0., value)
} else {
current_position = point(0., value);
}
minimum = minimum.min(current_position);
maximum = maximum.max(current_position);
}
}
_ => {}
}
}
Box2D::new(minimum, maximum)
}
pub struct PostprocessConfig {}

@ -91,8 +91,8 @@ impl<'input> Turtle for GCodeTurtle<'input> {
self.tool_off();
self.program.append(
&mut command!(RapidPositioning {
X: to.x as f64,
Y: to.y as f64,
X: to.x,
Y: to.y,
})
.into_token_vec(),
);

File diff suppressed because it is too large Load Diff

@ -489,7 +489,6 @@ G1 X67.59935302734368 Y227.98354431152342 F300
G1 X67.74791015624993 Y227.7720947265625 F300
G1 X67.89989501953119 Y227.56261535644532 F300
G1 X68.05499999999994 Y227.355 F300
G1 X68.21291748046868 Y227.14914245605465 F300
G1 X68.37333984374993 Y226.9449365234375 F300
G1 X68.70046874999994 Y226.54105468749998 F300
G1 X69.03392578124993 Y226.1425048828125 F300
@ -967,7 +966,6 @@ G1 X195.61218411960067 Y260.15298861228797 F300
G2 X196.1597977674986 Y259.4830481382608 R15.241554855625742 F300
G1 X196.2901280342283 Y259.3115556322605 F300
G1 X196.41783627781308 Y259.13851716857494 F300
G1 X196.5429261157294 Y258.9639612458603 F300
G1 X196.66540116545366 Y258.78791636277253 F300
G2 X197.55150328551127 Y257.32937456337095 R15.608580560381677 F300
G2 X198.2723426277259 Y255.79195452600038 R16.104035958950995 F300
@ -1389,7 +1387,6 @@ G3 X81.09374999999994 Y219.13000000000005 R10.137447148310702 F300
G3 X80.50564453124994 Y219.91058593750003 R15.061548260457004 F300
G1 X80.1940795898437 Y220.29037597656253 F300
G1 X79.87546874999994 Y220.66468750000004 F300
G1 X79.55328369140618 Y221.03466308593755 F300
G1 X79.23099609374994 Y221.40144531250004 F300
G1 X78.91207763671869 Y221.76617675781253 F300
G1 X78.59999999999994 Y222.13000000000005 F300
@ -1420,12 +1417,11 @@ G1 X73.20781608581535 Y230.63473358154303 F300
G1 X73.43276855468741 Y230.42501953125006 F300
G1 X73.6554926300048 Y230.21247100830084 F300
G1 X73.8761151123046 Y229.9972436523438 F300
G2 X74.31156249999991 Y229.55937500000005 R31.993776394915674 F300
G1 X74.31156249999991 Y229.55937500000005 F300
G1 X74.74012512207022 Y229.11265869140632 F300
G1 X75.16281738281239 Y228.65833984375007 F300
G1 X75.58065368652333 Y228.1976635742188 F300
G1 X75.9946484374999 Y227.73187500000006 F300
G1 X76.40581604003896 Y227.26221923828132 F300
G1 X76.8151708984374 Y226.78994140625008 F300
G1 X77.63249999999991 Y225.8425000000001 F300
G1 X78.0425030517577 Y225.36982666015632 F300
@ -1702,18 +1698,18 @@ G1 X109.63124999999998 Y74.51097656250008 F300
G1 X109.94999999999997 Y75.38000000000008 F300
G3 X119.86999999999996 Y75.8300000000001 R34.67999999999999 F300
G2 X120.16999999999994 Y65.83000000000013 R208.6745031467225 F300
G1 X120.17512963685566 Y65.6208535043148 F300
G1 X120.19030136124293 Y65.41833609215979 F300
G1 X120.21518939389188 Y65.22244845131678 F300
G1 X120.24946795553258 Y65.0331912695675 F300
G3 X120.34489354870982 Y64.67457103447687 R3.3954763834302315 F300
G3 X120.4739719066156 Y64.34248088914157 R3.196215757957963 F300
G1 X120.17512963685567 Y65.62085350431482 F300
G1 X120.19030136124296 Y65.41833609215982 F300
G1 X120.21518939389189 Y65.22244845131681 F300
G1 X120.2494679555326 Y65.03319126956752 F300
G3 X120.34489354870982 Y64.67457103447688 R3.395476383577339 F300
G3 X120.4739719066156 Y64.34248088914157 R3.1962157580294677 F300
G3 X120.63409679509087 Y64.03692633581528 R3.081684915236215 F300
G3 X120.82266197997657 Y63.7579128767517 R3.0400795203445585 F300
G3 X121.03706122711367 Y63.505446014204495 R3.0605121562668027 F300
G3 X121.27468830234307 Y63.27953125042734 R3.132332883657807 F300
G3 X122.40535020100305 Y62.641503228092816 R3.4593563329277064 F300
G3 X123.69915861641623 Y62.42874896637343 R3.9852670554719727 F300
G3 X121.03706122711364 Y63.505446014204495 R3.060512156227512 F300
G3 X121.27468830234307 Y63.27953125042733 R3.1323328836971776 F300
G3 X122.40535020100305 Y62.641503228092816 R3.459356332923317 F300
G3 X123.69915861641623 Y62.42874896637343 R3.985267055472677 F300
G3 X124.97580757141122 Y62.636984156476935 R3.9525651606106593 F300
G3 X126.08750355094628 Y63.26191686267471 R3.3913542048874707 F300
G3 X126.53106290994864 Y63.73075125425609 R3.024970727673866 F300
@ -1725,54 +1721,6 @@ G1 X127.12629541143723 Y65.1670482256363 F300
G1 X127.15065249261819 Y65.35925226986396 F300
G1 X127.16549788462454 Y65.55798185239833 F300
G1 X127.17051639205022 Y65.76323763867657 F300
G1 X127.16549788462454 Y65.55798185239833 F300
G1 X127.15065249261819 Y65.35925226986396 F300
G1 X127.12629541143723 Y65.1670482256363 F300
G1 X127.09274183648769 Y64.98136905427816 F300
G2 X126.99930598690709 Y64.62958266842182 R3.3310736229292974 F300
G2 X126.8728665071248 Y64.30388778879761 R3.1309432050181942 F300
G2 X126.71594496038918 Y64.00427909190816 R3.014932824909623 F300
G2 X126.53106290994862 Y63.73075125425608 R2.9715497695807134 F300
G2 X126.08750355094627 Y63.261916862674695 R3.024970727664751 F300
G2 X124.9758075714112 Y62.63698415647693 R3.391354204876731 F300
G2 X123.69915861641618 Y62.42874896637345 R3.9525651605878447 F300
G1 X123.67025819602509 Y62.4288510090401 F300
G2 X122.38500713967211 Y62.64854777371586 R3.979239053340435 F300
G2 X121.2638306862578 Y63.28886912213222 R3.4512925652615976 F300
G2 X121.02840072214528 Y63.51463145756922 R3.128496577907308 F300
G2 X120.81604374907099 Y63.7666557675723 R3.0588174344327963 F300
G2 X120.62932343270587 Y64.04493663975792 R3.040344540085188 F300
G2 X120.47080343872085 Y64.34946866174253 R3.083550688764618 F300
G2 X120.34304743278682 Y64.6802464211426 R3.199160794229291 F300
G2 X120.24861908057471 Y65.03726450557457 R3.398812753516823 F300
G1 X120.21470392013654 Y65.22561197830768 F300
G1 X120.19008204775545 Y65.42051750265492 F300
G1 X120.17507392164029 Y65.62198040206837 F300
G1 X120.16999999999994 Y65.83000000000011 F300
G1 X120.17512401580805 Y65.62096691131603 F300
G1 X120.1902792358398 Y65.41855560302746 F300
G1 X120.2151404190063 Y65.2227667617799 F300
G1 X120.24938232421871 Y65.03360107421886 F300
G3 X120.34470733642574 Y64.6751419067384 R3.3958113315836638 F300
G3 X120.47365234374995 Y64.34318359375013 R3.196510747434815 F300
G3 X120.63361541748041 Y64.0377316284181 R3.0818706925195642 F300
G3 X120.8219946289062 Y63.75879150390638 R3.0401036529116507 F300
G3 X121.03618804931635 Y63.50636871337905 R3.060338653333725 F300
G3 X121.27359374999995 Y63.28046875000014 R3.131943707175145 F300
G3 X122.40330078124995 Y62.64220703125015 R3.4585439068006796 F300
G3 X123.69624999999994 Y62.42875000000015 R3.984671200276607 F300
G1 X123.69915861641621 Y62.428748966373426 F300
G3 X124.97580757141122 Y62.636984156476935 R3.952565160608237 F300
G3 X126.08750355094628 Y63.26191686267471 R3.3913542048883696 F300
G3 X126.53106290994863 Y63.73075125425609 R3.0249707276541447 F300
G3 X126.71594496038917 Y64.00427909190816 R2.971549769560067 F300
G3 X126.87286650712478 Y64.30388778879762 R3.014932825305478 F300
G3 X126.99930598690709 Y64.62958266842185 R3.1309432050453263 F300
G3 X127.09274183648768 Y64.98136905427818 R3.331073622533537 F300
G1 X127.12629541143721 Y65.1670482256363 F300
G1 X127.15065249261818 Y65.35925226986396 F300
G1 X127.16549788462451 Y65.55798185239833 F300
G1 X127.1705163920502 Y65.76323763867659 F300
G1 X127.16999999999993 Y65.83000000000015 F300
G3 X126.90499999999992 Y75.69000000000017 R183.51220847229214 F300
G3 X126.5737499999999 Y80.61250000000017 R183.66928865945144 F300
@ -2037,9 +1985,8 @@ G1 X175.41999999999985 Y92.80000000000015 F300
G1 X175.59999999999982 Y92.61000000000018 F300
G3 X175.7099999999998 Y92.4300000000002 R0.9999999999999999 F300
G3 X175.72468749999976 Y92.37000000000018 R0.14629201939153627 F300
G3 X175.75999999999976 Y92.3100000000002 R0.39420890621379234 F300
G1 X175.75999999999976 Y92.3100000000002 F300
G2 X175.83999999999978 Y92.19000000000023 R1.0410984798146237 F300
G1 X175.8516015624998 Y92.16414062500021 F300
G1 X175.8578124999998 Y92.14437500000022 F300
G1 X175.86249999999978 Y92.11000000000023 F300
G1 X175.87093749999974 Y92.06062500000024 F300
@ -2065,7 +2012,7 @@ G1 X175.16999999999956 Y89.58000000000047 F300
G2 X174.94999999999953 Y89.3200000000005 R2.7499999999999996 F300
G2 X174.2899999999995 Y88.65000000000055 R8.359999999999998 F300
G2 X174.0274999999995 Y88.41625000000057 R6.731043949826764 F300
G2 X173.7499999999995 Y88.19000000000057 R10.351655170713371 F300
G1 X173.7499999999995 Y88.19000000000057 F300
G1 X173.62001631343787 Y88.08526092544612 F300
G1 X173.57427775451603 Y88.0453460786226 F300
G1 X173.57436783510963 Y88.04527270802494 F300
@ -2140,45 +2087,6 @@ G3 X191.71874999999943 Y87.55843750000061 R9.794885217747686 F300
G3 X192.48164062499944 Y86.86027343750062 R8.35318792355267 F300
G3 X193.31999999999942 Y86.28000000000063 R6.922973123917611 F300
G3 X192.1799999999994 Y66.61000000000067 R140.52999999999997 F300
G1 X192.18343536376898 Y66.47157226562558 F300
G1 X192.19359619140567 Y66.33753906250061 F300
G3 X192.23322265624944 Y66.08265625000062 R2.4164058388744185 F300
G3 X192.29713623046817 Y65.84535156250064 R2.224535710955547 F300
G3 X192.38359374999942 Y65.62562500000065 R2.098321898553023 F300
G3 X192.4908520507807 Y65.42347656250067 R2.0288928085744726 F300
G3 X192.6171679687494 Y65.23890625000067 R2.0084237138987646 F300
G3 X192.9199999999994 Y64.9225000000007 R2.057154796368932 F300
G3 X193.6776562499994 Y64.5006250000007 R2.3241991283168604 F300
G3 X194.5449999999994 Y64.3600000000007 R2.7047120026977027 F300
G3 X195.40351228726723 Y64.4982714850653 R2.6892527308508187 F300
G3 X196.15140738648336 Y64.91308594025915 R2.2912474948089847 F300
G3 X196.44988175049366 Y65.22419678165453 R2.0208353543779447 F300
G3 X196.5743020815857 Y65.40567810580183 R1.9715208866669631 F300
G3 X196.67991172939702 Y65.6044433655822 R1.9914140219967007 F300
G3 X196.76501110692374 Y65.82049256099566 R2.0598714578874446 F300
G3 X196.827900627162 Y66.05382569204218 R2.18452814656595 F300
G3 X196.86688070310782 Y66.30444275872179 R2.3740688043513343 F300
G3 X196.88025174775728 Y66.57234376103449 R2.638554935433838 F300
G2 X196.86688070310782 Y66.30444275872179 R2.6385549354338176 F300
G2 X196.827900627162 Y66.05382569204218 R2.3740688043513885 F300
G2 X196.76501110692374 Y65.82049256099566 R2.1845281465659268 F300
G2 X196.67991172939696 Y65.6044433655822 R2.0598714575761505 F300
G2 X196.57430208158564 Y65.40567810580183 R1.991414021976245 F300
G2 X196.44988175049366 Y65.22419678165453 R1.9715208866132286 F300
G2 X196.15140738648336 Y64.91308594025915 R2.020835354158665 F300
G2 X195.40351228726723 Y64.4982714850653 R2.2912474948091712 F300
G2 X194.5449999999994 Y64.3600000000007 R2.6892527308505905 F300
G1 X194.53012587387838 Y64.36003972177176 F300
G2 X193.66718902956345 Y64.50419251156414 R2.7013314041597787 F300
G2 X192.9144143355866 Y64.92723682119976 R2.319720518429372 F300
G2 X192.61376346695562 Y65.24334329595877 R2.0555147682468116 F300
G2 X192.48839664482608 Y65.42754261332358 R2.008383748072623 F300
G2 X192.3819639422859 Y65.6291726506786 R2.029710728396055 F300
G2 X192.29618664293412 Y65.8482334080238 R2.09972716733107 F300
G2 X192.23278603036977 Y66.08472488535922 R2.2261732929586824 F300
G2 X192.19348338819185 Y66.33864708268483 R2.4178244666791087 F300
G1 X192.18340670712251 Y66.47214470134396 F300
G1 X192.17999999999944 Y66.61000000000065 F300
G1 X192.18343536376898 Y66.47157226562567 F300
G1 X192.19359619140567 Y66.33753906250067 F300
G3 X192.23322265624944 Y66.08265625000067 R2.416405838904424 F300
@ -2188,16 +2096,16 @@ G3 X192.4908520507807 Y65.42347656250067 R2.0288928073153905 F300
G3 X192.61716796874944 Y65.2389062500007 R2.0084237138379906 F300
G3 X192.91999999999942 Y64.9225000000007 R2.0571547963224575 F300
G3 X193.67765624999942 Y64.5006250000007 R2.324199128313217 F300
G3 X194.54499999999942 Y64.3600000000007 R2.7047120027040195 F300
G3 X195.4035122872673 Y64.4982714850653 R2.6892527308490863 F300
G3 X196.1514073864834 Y64.91308594025915 R2.291247494815205 F300
G3 X196.44988175049372 Y65.22419678165453 R2.020835354349134 F300
G3 X196.5743020815857 Y65.40567810580183 R1.9715208842252911 F300
G3 X196.67991172939702 Y65.60444336558221 R1.9914140221747894 F300
G3 X196.76501110692377 Y65.82049256099566 R2.0598714576141406 F300
G3 X196.82790062716202 Y66.05382569204221 R2.1845281468470725 F300
G3 X196.86688070310782 Y66.30444275872182 R2.3740688040850197 F300
G3 X196.8802517477573 Y66.5723437610345 R2.6385549366519068 F300
G3 X194.5449999999994 Y64.3600000000007 R2.7047120027151585 F300
G3 X195.40351228726723 Y64.4982714850653 R2.6892527308508187 F300
G3 X196.15140738648336 Y64.91308594025915 R2.2912474948089847 F300
G3 X196.44988175049366 Y65.22419678165453 R2.0208353543779447 F300
G3 X196.5743020815857 Y65.40567810580183 R1.9715208866669631 F300
G3 X196.67991172939702 Y65.6044433655822 R1.9914140219967007 F300
G3 X196.76501110692374 Y65.82049256099566 R2.0598714578874446 F300
G3 X196.827900627162 Y66.05382569204218 R2.18452814656595 F300
G3 X196.86688070310782 Y66.30444275872179 R2.3740688043513343 F300
G3 X196.88025174775728 Y66.57234376103449 R2.638554935433838 F300
G1 X196.87999999999937 Y66.6100000000007 F300
G2 X197.99999999999997 Y85.73000000000005 R132.75999999999996 F300
G3 X201.67999999999995 Y88.41000000000008 R8.249999999999998 F300
@ -2254,7 +2162,7 @@ G3 X47.229999999999976 Y52.59000000000009 R3.4899999999999998 F300
G3 X43.709999999999965 Y49.07000000000011 R3.5899999999999994 F300
G2 X43.581816406249956 Y48.06810546875012 R12.61657647019767 F300
G2 X43.38203124999996 Y47.071718750000116 R16.613730432011668 F300
G2 X43.26103759765621 Y46.57526123046887 R21.6615756982986 F300
G1 X43.26103759765621 Y46.57526123046887 F300
G1 X43.12904296874996 Y46.079785156250125 F300
G1 X42.98834716796871 Y45.58515869140638 F300
G1 X42.84124999999996 Y45.09125000000013 F300
@ -2325,7 +2233,7 @@ G1 X49.86377777986809 Y14.361369311308458 F300
G1 X49.89993742444604 Y14.365195109502963 F300
G1 X49.92029870919092 Y14.371054190989192 F300
G1 X49.92668140955985 Y14.378523352172467 F300
G3 X49.91843620305571 Y14.389026198684792 R0.014421379138001572 F300
G1 X49.91843620305571 Y14.389026198684792 F300
G1 X49.895802871499995 Y14.400507999318549 F300
G1 X49.81998556097396 Y14.423475037894503 F300
G1 X49.724456933462 Y14.441557617788648 F300
@ -2347,7 +2255,7 @@ G2 X47.711249999999936 Y17.15375000000052 R6.92001149252184 F300
G2 X47.58337890624993 Y17.76529296875052 R9.01593378352431 F300
G2 X47.49171874999993 Y18.388906250000513 R12.248511721736854 F300
G2 X47.42701171874993 Y19.016503906250506 R17.91895462951365 F300
G2 X47.37999999999993 Y19.640000000000498 R31.48482635788041 F300
G1 X47.37999999999993 Y19.640000000000498 F300
G2 X47.31999999999992 Y26.76000000000053 R70.38999999999999 F300
G2 X51.429999999999914 Y25.130000000000564 R30.819999999999997 F300
G1 X52.00589843749992 Y24.85097656250057 F300
@ -2423,7 +2331,6 @@ G2 X72.30605151400263 Y40.61641956634448 R42.10758547370332 F300
G2 X72.13086908402192 Y39.663837024779866 R52.26052317773906 F300
G1 X72.03697756052108 Y39.18851983441776 F300
G1 X71.93935878534269 Y38.71377688877877 F300
G1 X71.83836747398391 Y38.239551831195115 F300
G1 X71.73435834194188 Y37.76578830499905 F300
G1 X71.51870547779652 Y36.81942042009858 F300
G1 X71.2952379168836 Y35.87422238073521 F300
@ -2457,7 +2364,6 @@ G1 X75.18540121537245 Y13.20792410447959 F300
G1 X75.20262594779132 Y13.410633548670019 F300
G1 X75.20999999999994 Y13.620000000000175 F300
G2 X77.20999999999992 Y30.33000000000021 R72.88999999999999 F300
G1 X77.44565425518357 Y31.31877023565753 F300
G1 X77.68388159725379 Y32.30820169497295 F300
G1 X77.92222917367118 Y33.29851856339363 F300
G1 X78.15824413189637 Y34.289945026366695 F300
@ -2580,7 +2486,6 @@ G1 X92.83316520114046 Y34.44760131961206 F300
G1 X93.02073022947386 Y35.22476654903329 F300
G1 X93.4040878227427 Y36.77713064960421 F300
G1 X93.79563945598412 Y38.32701256089231 F300
G1 X94.19205391860189 Y39.87457982899266 F300
G1 X94.58999999999988 Y41.42000000000033 F300
G2 X99.87999999999987 Y39.50000000000037 R12.359999999999998 F300
G2 X102.22999999999985 Y33.7800000000004 R5.059999999999999 F300
@ -2735,7 +2640,6 @@ G1 X149.0606436157226 Y40.908610229492254 F300
G1 X148.94263671874992 Y40.369882812500066 F300
G1 X148.82093109130852 Y39.831931762695376 F300
G1 X148.69577392578117 Y39.29470214843758 F300
G1 X148.5674124145507 Y38.758139038086014 F300
G1 X148.4360937499999 Y38.222187500000075 F300
G1 X148.16557373046868 Y37.15189941406258 F300
G1 X147.8861914062499 Y36.08339843750008 F300
@ -2779,7 +2683,6 @@ G2 X150.6710937499996 Y17.95296875000072 R55.345636354545896 F300
G2 X150.8475976562496 Y18.966660156250736 R65.41319733522946 F300
G1 X150.94146728515585 Y19.47191650390699 F300
G1 X151.0387499999996 Y19.976250000000746 F300
G1 X151.13919677734333 Y20.479763183594503 F300
G1 X151.24255859374955 Y20.98255859375076 F300
G1 X151.45703124999957 Y21.986406250000762 F300
G1 X151.6801757812496 Y22.98861328125076 F300
@ -2977,7 +2880,6 @@ G1 X190.02198913574205 Y36.20292755126965 F300
G1 X189.83556152343738 Y35.75765869140638 F300
G1 X189.65290588378892 Y35.310694274902474 F300
G1 X189.47371093749985 Y34.862285156250124 F300
G1 X189.2976654052733 Y34.41268218994153 F300
G1 X189.12445800781236 Y33.962136230468865 F300
G1 X188.78531249999986 Y33.05921875000012 F300
G1 X188.45378417968735 Y32.15553955078137 F300
@ -3155,10 +3057,8 @@ G1 X223.66999999999962 Y39.14000000000041 F300
G1 X223.5049999999996 Y39.421250000000434 F300
G1 X223.4140624999996 Y39.56890625000044 F300
G1 X223.33999999999958 Y39.68000000000043 F300
G1 X223.32433837890585 Y39.705002441406684 F300
G1 X223.30751953124957 Y39.73798828125044 F300
G1 X223.27140624999956 Y39.823281250000434 F300
G1 X223.23365234374955 Y39.92662109375044 F300
G1 X223.19624999999957 Y40.03875000000045 F300
G1 X223.13046874999958 Y40.25234375000046 F300
G1 X223.08999999999955 Y40.39000000000047 F300
@ -3184,7 +3084,6 @@ G3 X214.59050800623007 Y47.24949209699645 R3.5355339059327378 F300
G1 X214.6399999999994 Y47.19000000000062 F300
G1 X214.63999999999936 Y47.19000000000065 F300
G2 X214.81999999999934 Y46.52000000000069 R7.383741218294527 F300
G1 X214.85093749999933 Y46.370468750000704 F300
G1 X214.87249999999932 Y46.253750000000714 F300
G1 X214.90999999999931 Y46.04000000000073 F300
G1 X214.9099999999993 Y45.96000000000075 F300
@ -3204,7 +3103,6 @@ G1 X230.29016876220695 Y34.7873043823244 F300
G1 X230.51152587890618 Y34.98943115234393 F300
G1 X230.73042572021478 Y35.19474761962909 F300
G1 X230.9469726562499 Y35.402871093750186 F300
G1 X231.1612710571288 Y35.61341888427753 F300
G1 X231.37342529296865 Y35.82600830078144 F300
G1 X231.79171874999992 Y36.25578125000018 F300
G1 X232.20268798828116 Y36.68912841796894 F300

@ -1,8 +1,8 @@
[package]
name = "svg2gcode-web"
version = "0.0.2"
version = "0.0.3"
authors = ["Sameer Puri <crates@purisa.me>"]
edition = "2018"
edition = "2021"
description = "Convert vector graphics to g-code for pen plotters, laser engravers, and other CNC machines"
repository = "https://github.com/sameer/svg2gcode"
homepage = "https://sameer.github.io/svg2gcode/"
@ -11,24 +11,23 @@ license = "MIT"
[dependencies]
wasm-bindgen = "0.2"
svg2gcode = { path = "../lib", features = ["serde"] }
roxmltree = "0"
g-code = "0"
roxmltree = "0.18"
g-code = "0.3"
codespan-reporting = "0.11"
codespan = "0.11"
serde = "1"
paste = "1"
log = "0"
svgtypes = "0"
log = "0.4"
svgtypes = "0.11"
serde_json = "1"
thiserror = "1.0"
yew = { git = "https://github.com/yewstack/yew.git" }
yewdux-functional = { git = "https://github.com/intendednull/yewdux.git" }
yewdux-input = { git = "https://github.com/intendednull/yewdux.git" }
yewdux = { git = "https://github.com/intendednull/yewdux.git" }
yew = { version ="0.20", features = ["csr"] }
yewdux = "0.9.2"
web-sys = { version = "0.3", features = [] }
wasm-logger = "0.2"
gloo-file = { version = "0.1", features = ["futures"] }
gloo-timers = "0.1"
base64 = "0.13"
gloo-file = { version = "0.2", features = ["futures"] }
gloo-timers = "0.2"
base64 = "0.21"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"

@ -2,12 +2,12 @@ use codespan_reporting::term::{emit, termcolor::NoColor, Config};
use g_code::parse::{into_diagnostic, snippet_parser};
use gloo_timers::callback::Timeout;
use paste::paste;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yewdux_functional::use_store;
use yewdux_input::*;
use yewdux::functional::{use_store, use_store_value};
use crate::{
state::{AppState, AppStore, FormStore},
state::{AppState, FormState},
ui::{FormGroup, TextArea},
};
@ -23,13 +23,14 @@ macro_rules! gcode_input {
#[function_component([<$name Input>])]
pub fn [<$name:snake:lower _input>]() -> Html {
const VALIDATION_TIMEOUT: u32 = 350;
let app = use_store::<AppStore>();
let form = use_store::<FormStore>();
let app_state = use_store_value::<AppState>();
let (form_state, form_dispatch) = use_store::<FormState>();
let timeout = use_state::<Option<Timeout>, _>(|| None);
let oninput = {
let timeout = timeout.clone();
form.dispatch().input(move |state, value| {
form_dispatch.reduce_mut_callback_with(move |state, event: InputEvent| {
let value = event.target_unchecked_into::<HtmlInputElement>().value();
let res = Some(match snippet_parser(&value) {
Ok(_) => Ok(value),
Err(err) => {
@ -45,7 +46,7 @@ macro_rules! gcode_input {
Err(String::from_utf8_lossy(buf.get_ref().as_slice()).to_string())
}
}).filter(|res| {
!res.as_ref().ok().map(|value| value.is_empty()).unwrap_or(false)
!res.as_ref().ok().map_or(false, |value| value.is_empty())
});
let timeout_inner = timeout.clone();
@ -56,10 +57,10 @@ macro_rules! gcode_input {
})
};
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).as_ref().map(Result::is_ok)).flatten()}>
<FormGroup success={form_state.$form_accessor $([$form_idx])?.as_ref().map(Result::is_ok)}>
<TextArea<String, String> label=$label desc=$desc
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())}
default={(app_state.$app_accessor $([$app_idx])?).clone()}
parsed={(form_state.$form_accessor $([$form_idx])?).clone().filter(|_| timeout.is_none())}
oninput={oninput}
/>
</FormGroup>

@ -1,12 +1,10 @@
use paste::paste;
use std::num::ParseFloatError;
use yew::prelude::*;
use yewdux::prelude::BasicStore;
use yewdux_functional::use_store;
use yewdux_input::*;
use yewdux::functional::{use_store, use_store_value};
use crate::{
state::{AppState, AppStore, FormState},
state::{AppState, FormState},
ui::*,
};
@ -21,14 +19,25 @@ macro_rules! form_input {
paste! {
#[function_component([<$name Input>])]
pub fn [<$name:snake:lower _input>]() -> Html {
let app = use_store::<AppStore>();
let form = use_store::<BasicStore<FormState>>();
let oninput = form.dispatch().input(|state, value| state.$form_accessor $([$form_idx])? = value.parse::<f64>());
let app_state = use_store_value::<AppState>();
let (form_state, form_dispatch) = use_store::<FormState>();
let oninput = form_dispatch.reduce_mut_callback_with(|state, event: InputEvent| {
let value = event.target_unchecked_into::<web_sys::HtmlInputElement>().value();
let parsed = value.parse::<f64>();
// Handle Option origins
$(
let _ = $app_idx;
let parsed = if value.is_empty() { None } else { Some(parsed) };
)?
state.$form_accessor $([$form_idx])? = parsed;
});
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).is_ok())}>
// unwrap_or(&Ok(0.)) is just a macro hack to make None a valid state
<FormGroup success={form_state.$form_accessor $([$form_idx] .as_ref().unwrap_or(&Ok(0.)))?.is_ok()}>
<Input<f64, ParseFloatError> label=$label desc=$desc
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())}
default={app_state.$app_accessor $([$app_idx])?}
parsed={form_state.$form_accessor $([$form_idx])?.clone()}
oninput={oninput}
/>
</FormGroup>
@ -62,12 +71,12 @@ form_input! {
"Origin X",
"X-axis coordinate for the bottom left corner of the machine",
origin => 0,
settings.postprocess.origin => 0,
settings.conversion.origin => 0,
}
OriginY {
"Origin Y",
"Y-axis coordinate for the bottom left corner of the machine",
origin => 1,
settings.postprocess.origin => 1,
settings.conversion.origin => 1,
}
}

@ -1,17 +1,19 @@
use gloo_file::futures::{read_as_bytes, read_as_text};
use gloo_file::{
callbacks::{read_as_bytes, FileReader},
futures::read_as_text,
};
use js_sys::TypeError;
use roxmltree::Document;
use std::{convert::TryInto, path::Path};
use svg2gcode::Settings;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, FileList, HtmlElement, Response};
use web_sys::{window, Event, FileList, HtmlElement, HtmlInputElement, Response};
use yew::prelude::*;
use yewdux::prelude::{BasicStore, Dispatcher};
use yewdux_functional::use_store;
use yewdux::{functional::use_store, prelude::Dispatch};
use crate::{
state::{AppStore, FormState, Svg},
state::{AppState, FormState, Svg},
ui::{
Button, ButtonStyle, Checkbox, FileUpload, FormGroup, HyperlinkButton, Icon, IconName,
Input, InputType, Modal,
@ -26,59 +28,53 @@ use inputs::*;
#[function_component(SettingsForm)]
pub fn settings_form() -> Html {
let app = use_store::<AppStore>();
let form = use_store::<BasicStore<FormState>>();
let app_dispatch = Dispatch::<AppState>::new();
let (form_state, form_dispatch) = use_store::<FormState>();
// let handle = use_state(|| None);
let disabled = form
.state()
.map(|state| {
state.tolerance.is_err()
|| state.feedrate.is_err()
|| state.dpi.is_err()
|| state.origin.iter().all(Result::is_err)
|| state
.tool_on_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| state
.tool_off_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| state
.begin_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| state
.end_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
})
.unwrap_or(true);
let disabled = form_state.tolerance.is_err()
|| form_state.feedrate.is_err()
|| form_state.dpi.is_err()
|| form_state
.origin
.iter()
.all(|opt| opt.as_ref().map_or(false, |r| r.is_err()))
|| form_state
.tool_on_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| form_state
.tool_off_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| form_state
.begin_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false)
|| form_state
.end_sequence
.as_ref()
.map(Result::is_err)
.unwrap_or(false);
let close_ref = NodeRef::default();
let close_ref = use_node_ref();
// MDN says on input should fire for checkboxes
// but historically hasn't been the case, on change is safer.
let on_circular_interpolation_change =
form.dispatch().reduce_callback_with(|form, change_data| {
if let ChangeData::Value(_) = change_data {
form.circular_interpolation = !form.circular_interpolation;
}
form_dispatch.reduce_mut_callback_with(|form, event: Event| {
let checkbox = event.target_unchecked_into::<HtmlInputElement>();
form.circular_interpolation = checkbox.checked();
});
let circular_interpolation_checked = form
.state()
.map(|state| state.circular_interpolation)
.unwrap_or(false);
let circular_interpolation_checked = form_state.circular_interpolation;
let save_onclick = {
let close_ref = close_ref.clone();
app.dispatch().reduce_callback(move |app| {
if let (false, Some(form)) = (disabled, form.state()) {
app.settings = form.as_ref().try_into().unwrap();
app_dispatch.reduce_mut_callback(move |app| {
if !disabled {
app.settings = form_state.as_ref().try_into().unwrap();
// TODO: this is a poor man's crutch for closing the Modal.
// There is probably a better way.
if let Some(element) = close_ref.cast::<HtmlElement>() {
@ -102,6 +98,7 @@ pub fn settings_form() -> Html {
{"local storage"}
</a>
{"."}
{" Reloading the page clears unsaved settings."}
</p>
</>
)
@ -114,7 +111,7 @@ pub fn settings_form() -> Html {
<OriginYInput/>
<FormGroup>
<Checkbox
label="Enable circular interpolation"
label="Enable circular interpolation (experimental)"
desc="Please check if your machine supports G2/G3 commands before enabling this"
checked={circular_interpolation_checked}
onchange={on_circular_interpolation_change}
@ -146,10 +143,10 @@ pub fn settings_form() -> Html {
/>
{" "}
<HyperlinkButton
ref={close_ref}
title="Close"
href="#close"
style={ButtonStyle::Default}
noderef={close_ref}
/>
</>
)
@ -160,36 +157,45 @@ pub fn settings_form() -> Html {
#[function_component(ImportExportModal)]
pub fn import_export_modal() -> Html {
let app = use_store::<AppStore>();
let app_dispatch = Dispatch::<AppState>::new();
let form_dispatch = Dispatch::<FormState>::new();
let import_state = use_state(|| Option::<Result<Settings, String>>::None);
let import_reading = use_state(|| Option::<FileReader>::None);
let import_reading_setter = import_reading.setter();
let export_error = use_state(|| Option::<String>::None);
let export_onclick = {
let export_error = export_error.clone();
app.dispatch()
.reduce_callback(move |app| match serde_json::to_vec_pretty(&app.settings) {
app_dispatch.reduce_mut_callback(move |app| {
match serde_json::to_vec_pretty(&app.settings) {
Ok(settings_json_bytes) => {
let filename = "svg2gcode_settings";
let filepath = Path::new(&filename).with_extension("json");
crate::util::prompt_download(&filepath, &settings_json_bytes);
crate::util::prompt_download(filepath, settings_json_bytes);
}
Err(serde_json_err) => {
export_error.set(Some(serde_json_err.to_string()));
}
})
}
})
};
let close_ref = use_node_ref();
let settings_upload_onchange = {
let import_state = import_state.clone();
app.dispatch()
.future_callback_with(move |app, file_list: FileList| {
let import_state = import_state.clone();
async move {
let file = file_list.item(0).unwrap();
let filename = file.name();
Callback::from(move |file_list: FileList| {
let import_state = import_state.clone();
let res = read_as_bytes(&gloo_file::File::from(file))
.await
let file = file_list.item(0).unwrap();
let filename = file.name();
let import_reading_setter_inner = import_reading_setter.clone();
import_reading_setter.clone().set(Some(read_as_bytes(
&gloo_file::File::from(file),
move |res| {
let res = res
.map_err(|err| format!("Error reading {}: {}", &filename, err))
.and_then(|bytes| {
serde_json::from_slice::<Settings>(&bytes)
@ -204,22 +210,29 @@ pub fn import_export_modal() -> Html {
import_state.set(Some(Err(err)));
}
}
}
})
import_reading_setter_inner.set(None);
},
)));
})
};
let import_save_onclick = {
let import_state = import_state.clone();
app.dispatch().reduce_callback(move |app| {
let close_ref = close_ref.clone();
app_dispatch.reduce_mut_callback(move |app| {
if let Some(Ok(ref settings)) = *import_state {
app.settings = settings.clone();
app.settings = settings.clone();
// App only hydrates the form on start now, so need to do it again here
form_dispatch.reduce_mut(|form| *form = (&app.settings).into());
import_state.set(None);
// TODO: another way to close the modal?
if let Some(element) = close_ref.cast::<HtmlElement>() {
element.click();
}
}
})
};
let close_ref = NodeRef::default();
html! {
<Modal
id="import_export"
@ -243,9 +256,10 @@ pub fn import_export_modal() -> Html {
button={html_nested!(
<Button
style={ButtonStyle::Primary}
disabled={import_state.is_none()}
disabled={import_state.as_ref().map_or(true, |r| r.is_err()) || import_reading.is_some()}
title="Save"
onclick={import_save_onclick}
input_group=true
/>
)}
/>
@ -274,10 +288,10 @@ pub fn import_export_modal() -> Html {
footer={
html!(
<HyperlinkButton
ref={close_ref}
style={ButtonStyle::Default}
title="Close"
href="#close"
noderef={close_ref}
/>
)
}
@ -287,47 +301,44 @@ pub fn import_export_modal() -> Html {
#[function_component(SvgForm)]
pub fn svg_form() -> Html {
let app = use_store::<AppStore>();
let app_dispatch = Dispatch::<AppState>::new();
let file_upload_state = use_ref(Vec::default);
let file_upload_state = use_mut_ref(Vec::default);
let file_upload_state_cloned = file_upload_state.clone();
let file_upload_onchange =
app.dispatch()
.future_callback_with(move |app, file_list: FileList| {
let file_upload_state_cloned = file_upload_state_cloned.clone();
async move {
let mut results = Vec::with_capacity(file_list.length() as usize);
for file in (0..file_list.length()).filter_map(|i| file_list.item(i)) {
let filename = file.name();
results.push(
read_as_text(&gloo_file::File::from(file))
.await
.map_err(|err| err.to_string())
.and_then(|text| {
if let Some(err) = Document::parse(&text).err() {
Err(format!("Error parsing {}: {}", &filename, err))
} else {
Ok(Svg {
content: text,
filename,
dimensions: [None; 2],
})
}
}),
);
}
app.reduce(move |app| {
// Clear any errors from previous entry, add new successfully parsed SVGs
(*file_upload_state_cloned).borrow_mut().clear();
for result in results.iter() {
(*file_upload_state_cloned)
.borrow_mut()
.push(result.clone().map(|_| ()));
}
app.svgs.extend(results.drain(..).filter_map(Result::ok));
});
app_dispatch.reduce_mut_future_callback_with(move |app, file_list: FileList| {
let file_upload_state_cloned = file_upload_state_cloned.clone();
Box::pin(async move {
let mut results = Vec::with_capacity(file_list.length() as usize);
for file in (0..file_list.length()).filter_map(|i| file_list.item(i)) {
let filename = file.name();
results.push(
read_as_text(&gloo_file::File::from(file))
.await
.map_err(|err| err.to_string())
.and_then(|text| {
if let Some(err) = Document::parse(&text).err() {
Err(format!("Error parsing {}: {}", &filename, err))
} else {
Ok(Svg {
content: text,
filename,
dimensions: [None; 2],
})
}
}),
);
}
});
// Clear any errors from previous entry, add new successfully parsed SVGs
(*file_upload_state_cloned).borrow_mut().clear();
for result in results.iter() {
(*file_upload_state_cloned)
.borrow_mut()
.push(result.clone().map(|_| ()));
}
app.svgs.extend(results.drain(..).filter_map(Result::ok));
})
});
let file_upload_errors = file_upload_state
.borrow()
@ -348,8 +359,9 @@ pub fn svg_form() -> Html {
let url_input_oninput = {
let url_input_state = url_input_state.clone();
let url_input_parsed = url_input_parsed.clone();
Callback::from(move |url: InputData| {
url_input_state.set(Some(url.value));
Callback::from(move |event: InputEvent| {
let url = event.target_unchecked_into::<HtmlInputElement>();
url_input_state.set(Some(url.value()));
url_input_parsed.set(None);
})
};
@ -360,14 +372,14 @@ pub fn svg_form() -> Html {
let url_input_parsed = url_input_parsed.clone();
let url_add_loading = url_add_loading.clone();
app.dispatch().future_callback_with(move |app, _| {
app_dispatch.reduce_mut_future_callback_with(move |app, _| {
let url_input_state = url_input_state.clone();
let url_input_parsed = url_input_parsed.clone();
let url_add_loading = url_add_loading.clone();
url_add_loading.set(true);
let request_url = url_input_state.as_ref().unwrap().clone();
async move {
Box::pin(async move {
url_input_parsed.set(None);
let res = JsFuture::from(window().unwrap().fetch_with_str(&request_url))
.await
@ -387,12 +399,10 @@ pub fn svg_form() -> Html {
&response_url, err
))));
} else {
app.reduce(move |app| {
app.svgs.push(Svg {
content: text,
filename: response_url,
dimensions: [None; 2],
})
app.svgs.push(Svg {
content: text,
filename: response_url,
dimensions: [None; 2],
});
};
}
@ -404,7 +414,7 @@ pub fn svg_form() -> Html {
))));
}
}
}
})
})
};

@ -1,5 +1,6 @@
use std::{path::Path, rc::Rc};
use std::path::Path;
use base64::Engine;
use g_code::{
emit::{format_gcode_fmt, FormatOptions},
parse::snippet_parser,
@ -8,7 +9,6 @@ use log::Level;
use roxmltree::Document;
use svg2gcode::{svg2program, ConversionOptions, Machine};
use yew::prelude::*;
use yewdux::prelude::{Dispatch, Dispatcher};
mod forms;
mod state;
@ -19,224 +19,183 @@ use forms::*;
use state::*;
use ui::*;
use util::*;
struct App {
app_dispatch: Dispatch<AppStore>,
app_state: Rc<AppState>,
form_dispatch: Dispatch<FormStore>,
form_state: Rc<FormState>,
generating: bool,
link: ComponentLink<Self>,
}
enum AppMsg {
AppState(Rc<AppState>),
FormState(Rc<FormState>),
Generate,
Done,
}
impl Component for App {
type Message = AppMsg;
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
app_dispatch: Dispatch::bridge_state(link.callback(AppMsg::AppState)),
app_state: Default::default(),
form_dispatch: Dispatch::bridge_state(link.callback(AppMsg::FormState)),
form_state: Default::default(),
generating: false,
link,
}
use yewdux::prelude::{use_store, Dispatch};
// struct App {
// app_store: Rc<AppState>,
// form_state: Rc<FormState>,
// generating: bool,
// }
// enum AppMsg {
// AppState(Rc<AppState>),
// FormState(Rc<FormState>),
// Generate,
// Done,
// }
#[function_component]
fn App(_props: &()) -> Html {
let generating = use_state_eq(|| false);
let generating_setter = generating.setter();
let form_dispatch = Dispatch::<FormState>::new();
let (app_store, app_dispatch) = use_store::<AppState>();
// TODO: come up with a less awkward way to do this.
// Having separate stores is somewhat of an anti-pattern in Redux,
// but there's no easy way to do hydration after the app state is
// restored from local storage.
let hydrated_form = use_state(|| false);
if !*hydrated_form {
let hydrated_form_state = FormState::from(&app_store.settings);
form_dispatch.reduce_mut(|state| *state = hydrated_form_state);
hydrated_form.set(true);
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
AppMsg::AppState(app_state) => {
self.app_state = app_state;
true
}
AppMsg::FormState(form_state) => {
self.form_state = form_state;
true
}
AppMsg::Generate => {
self.generating = true;
let app_state = self.app_state.clone();
// TODO: once trunk and yew have better support for workers,
// pull this out into one so that the UI can actually
// show progress updates.
self.link.send_future(async move {
for svg in app_state.svgs.iter() {
let options = ConversionOptions {
dimensions: svg.dimensions,
};
let machine = Machine::new(
app_state.settings.machine.supported_functionality.clone(),
app_state
.settings
.machine
.tool_on_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_state
.settings
.machine
.tool_off_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_state
.settings
.machine
.begin_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_state
.settings
.machine
.end_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
);
let document = Document::parse(svg.content.as_str()).unwrap();
let program = svg2program(
&document,
&app_state.settings.conversion,
options,
machine,
);
let gcode = {
let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();
acc
};
let filepath = Path::new(svg.filename.as_str()).with_extension("gcode");
prompt_download(filepath, &gcode.as_bytes());
}
AppMsg::Done
});
true
let generate_disabled = *generating || app_store.svgs.is_empty();
let generate_onclick = {
let app_store = app_store.clone();
Callback::from(move |_| {
generating_setter.set(true);
for svg in app_store.svgs.iter() {
let options = ConversionOptions {
dimensions: svg.dimensions,
};
let machine = Machine::new(
app_store.settings.machine.supported_functionality.clone(),
app_store
.settings
.machine
.tool_on_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_store
.settings
.machine
.tool_off_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_store
.settings
.machine
.begin_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
app_store
.settings
.machine
.end_sequence
.as_deref()
.map(snippet_parser)
.transpose()
.unwrap(),
);
let document = Document::parse(svg.content.as_str()).unwrap();
let program =
svg2program(&document, &app_store.settings.conversion, options, machine);
let gcode = {
let mut acc = String::new();
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();
acc
};
let filepath = Path::new(svg.filename.as_str()).with_extension("gcode");
prompt_download(filepath, gcode.as_bytes());
}
AppMsg::Done => {
self.generating = false;
true
}
}
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let generate_disabled = self.generating || self.app_state.svgs.is_empty();
let generate_onclick = self.link.callback(|_| AppMsg::Generate);
// TODO: come up with a less awkward way to do this.
// Having separate stores is somewhat of an anti-pattern in Redux,
// but there's no easy way to do hydration after the app state is
// restored from local storage.
let hydrated_form_state = FormState::from(&self.app_state.settings);
let settings_hydrate_onclick = self.form_dispatch.reduce_callback_once(move |form| {
*form = hydrated_form_state;
});
html! {
<div class="container">
<div class={classes!("column")}>
<h1>
{ "svg2gcode" }
</h1>
<p>
{ env!("CARGO_PKG_DESCRIPTION") }
</p>
<SvgForm/>
<div class={classes!("card-container", "columns")}>
{
for self.app_state.svgs.iter().enumerate().map(|(i, svg)| {
let svg_base64 = base64::encode(svg.content.as_bytes());
let remove_svg_onclick = self.app_dispatch.reduce_callback_once(move |app| {
app.svgs.remove(i);
});
let footer = html!{
<Button
title="Remove"
style={ButtonStyle::Primary}
icon={
html_nested!(
<Icon name={IconName::Delete} />
)
}
onclick={remove_svg_onclick}
generating_setter.set(false);
})
};
html! {
<div class="container">
<div class={classes!("column")}>
<h1>
{ "svg2gcode" }
</h1>
<p>
{ env!("CARGO_PKG_DESCRIPTION") }
</p>
<SvgForm/>
<div class={classes!("card-container", "columns")}>
{
for app_store.svgs.iter().enumerate().map(|(i, svg)| {
let svg_base64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(svg.content.as_bytes());
let remove_svg_onclick = app_dispatch.reduce_mut_callback(move |app| {
app.svgs.remove(i);
});
let footer = html!{
<Button
title="Remove"
style={ButtonStyle::Primary}
icon={
html_nested!(
<Icon name={IconName::Delete} />
)
}
onclick={remove_svg_onclick}
/>
};
html!{
<div class={classes!("column", "col-6", "col-xs-12")}>
<Card
title={svg.filename.clone()}
img={html_nested!(
<img class="img-responsive" src={format!("data:image/svg+xml;base64,{}", svg_base64)} alt={svg.filename.clone()} />
)}
footer={footer}
/>
};
html!{
<div class={classes!("column", "col-6", "col-xs-12")}>
<Card
title={svg.filename.clone()}
img={html_nested!(
<img class="img-responsive" src={format!("data:image/svg+xml;base64,{}", svg_base64)} alt={svg.filename.clone()} />
)}
footer={footer}
/>
</div>
}
})
}
</div>
<ButtonGroup>
<Button
title="Generate G-Code"
style={ButtonStyle::Primary}
loading={self.generating}
icon={
html_nested! (
<Icon name={IconName::Download} />
)
</div>
}
disabled={generate_disabled}
onclick={generate_onclick}
/>
<HyperlinkButton
title="Settings"
style={ButtonStyle::Default}
icon={IconName::Edit}
onclick={settings_hydrate_onclick}
href="#settings"
/>
</ButtonGroup>
<SettingsForm/>
<ImportExportModal/>
</div>
<div class={classes!("text-right", "column")}>
<p>
{ "See the project " }
<a href={env!("CARGO_PKG_REPOSITORY")}>
{ "on GitHub" }
</a>
{" for support" }
</p>
})
}
</div>
<ButtonGroup>
<Button
title="Generate G-Code"
style={ButtonStyle::Primary}
loading={*generating}
icon={
html_nested! (
<Icon name={IconName::Download} />
)
}
disabled={generate_disabled}
onclick={generate_onclick}
/>
<HyperlinkButton
title="Settings"
style={ButtonStyle::Default}
icon={IconName::Edit}
href="#settings"
/>
</ButtonGroup>
<SettingsForm/>
<ImportExportModal/>
</div>
<div class={classes!("text-right", "column")}>
<p>
{ "See the project " }
<a href={env!("CARGO_PKG_REPOSITORY")}>
{ "on GitHub" }
</a>
{" for support" }
</p>
</div>
}
</div>
}
}
fn main() {
wasm_logger::init(wasm_logger::Config::new(Level::Info));
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

@ -4,13 +4,15 @@ use svg2gcode::{
ConversionConfig, MachineConfig, PostprocessConfig, Settings, SupportedFunctionality,
};
use svgtypes::Length;
use yewdux::prelude::{BasicStore, Persistent, PersistentStore};
use thiserror::Error;
use yewdux::store::Store;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Store)]
#[store]
pub struct FormState {
pub tolerance: Result<f64, ParseFloatError>,
pub feedrate: Result<f64, ParseFloatError>,
pub origin: [Result<f64, ParseFloatError>; 2],
pub origin: [Option<Result<f64, ParseFloatError>>; 2],
pub circular_interpolation: bool,
pub dpi: Result<f64, ParseFloatError>,
pub tool_on_sequence: Option<Result<String, String>>,
@ -26,8 +28,16 @@ impl Default for FormState {
}
}
#[derive(Debug, Error)]
pub enum FormStateConversionError {
#[error(transparent)]
Float(#[from] ParseFloatError),
#[error("could not parse gcode: {0}")]
GCode(String),
}
impl<'a> TryInto<Settings> for &'a FormState {
type Error = ParseFloatError;
type Error = FormStateConversionError;
fn try_into(self) -> Result<Settings, Self::Error> {
Ok(Settings {
@ -35,20 +45,37 @@ 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()?)],
origin: [
self.origin[0].clone().transpose()?,
self.origin[1].clone().transpose()?,
],
},
machine: MachineConfig {
supported_functionality: SupportedFunctionality {
circular_interpolation: self.circular_interpolation,
},
tool_on_sequence: self.tool_on_sequence.clone().and_then(Result::ok),
tool_off_sequence: self.tool_off_sequence.clone().and_then(Result::ok),
begin_sequence: self.begin_sequence.clone().and_then(Result::ok),
end_sequence: self.end_sequence.clone().and_then(Result::ok),
},
postprocess: PostprocessConfig {
origin: [self.origin[0].clone()?, self.origin[1].clone()?],
tool_on_sequence: self
.tool_on_sequence
.clone()
.transpose()
.map_err(FormStateConversionError::GCode)?,
tool_off_sequence: self
.tool_off_sequence
.clone()
.transpose()
.map_err(FormStateConversionError::GCode)?,
begin_sequence: self
.begin_sequence
.clone()
.transpose()
.map_err(FormStateConversionError::GCode)?,
end_sequence: self
.end_sequence
.clone()
.transpose()
.map_err(FormStateConversionError::GCode)?,
},
postprocess: PostprocessConfig {},
})
}
}
@ -63,21 +90,20 @@ impl From<&Settings> for FormState {
.supported_functionality
.circular_interpolation,
origin: [
Ok(settings.conversion.origin[0].unwrap_or(settings.postprocess.origin[0])),
Ok(settings.conversion.origin[1].unwrap_or(settings.postprocess.origin[1])),
settings.conversion.origin[0].map(Ok),
settings.conversion.origin[1].map(Ok),
],
dpi: Ok(settings.conversion.dpi),
tool_on_sequence: settings.machine.tool_on_sequence.clone().map(Result::Ok),
tool_off_sequence: settings.machine.tool_off_sequence.clone().map(Result::Ok),
begin_sequence: settings.machine.begin_sequence.clone().map(Result::Ok),
end_sequence: settings.machine.end_sequence.clone().map(Result::Ok),
tool_on_sequence: settings.machine.tool_on_sequence.clone().map(Ok),
tool_off_sequence: settings.machine.tool_off_sequence.clone().map(Ok),
begin_sequence: settings.machine.begin_sequence.clone().map(Ok),
end_sequence: settings.machine.end_sequence.clone().map(Ok),
}
}
}
pub type AppStore = PersistentStore<AppState>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Store)]
#[store(storage = "local", storage_tab_sync)]
pub struct AppState {
pub first_visit: bool,
pub settings: Settings,
@ -101,7 +127,3 @@ impl Default for AppState {
}
}
}
impl Persistent for AppState {}
pub type FormStore = BasicStore<FormState>;

@ -1,9 +1,9 @@
use std::fmt::Display;
use web_sys::{FileList, HtmlInputElement, MouseEvent};
use web_sys::{FileList, HtmlInputElement, MouseEvent, InputEvent, Event};
use yew::{
classes, function_component, html, use_state,
virtual_dom::{VChild, VNode},
Callback, ChangeData, Children, Html, InputData, NodeRef, Properties,
Callback, Children, Html, NodeRef, Properties, TargetCast, use_node_ref, use_force_update,
};
macro_rules! css_class_enum {
@ -57,7 +57,7 @@ where
#[prop_or(InputType::Text)]
pub r#type: InputType,
#[prop_or_default]
pub oninput: Callback<InputData>,
pub oninput: Callback<InputEvent>,
#[prop_or_default]
pub button: Option<VChild<Button>>,
}
@ -79,8 +79,17 @@ where
let error = props.parsed.as_ref().map(|x| x.is_err()).unwrap_or(false);
let id = props.label.to_lowercase().replace(' ', "-");
// To properly set the default value, we need to force a second render
// so the noderef becomes valid.
let first_render = use_state(|| true);
let trigger = use_force_update();
let applied_default_value = use_state(|| false);
let node_ref = use_state(NodeRef::default);
let node_ref = use_node_ref();
if *first_render {
first_render.set(false);
trigger.force_update();
}
if let (false, Some(default), Some(input_element)) = (
*applied_default_value,
@ -98,7 +107,7 @@ where
</label>
<div class={classes!(if props.button.is_some() { Some("input-group") } else { None })}>
<div class={classes!(if props.button.is_some() { Some("input-group") } else { None }, if success || error { Some("has-icon-right") } else { None })}>
<input id={id} class="form-input" type={props.r#type.to_string()} ref={(*node_ref).clone()}
<input id={id} class="form-input" type={props.r#type.to_string()} ref={node_ref.clone()}
oninput={props.oninput.clone()} placeholder={ props.placeholder.as_ref().map(ToString::to_string) }
/>
{
@ -134,7 +143,7 @@ pub struct CheckboxProps {
#[prop_or(false)]
pub checked: bool,
#[prop_or_default]
pub onchange: Callback<ChangeData>,
pub onchange: Callback<Event>,
}
#[function_component(Checkbox)]
@ -192,11 +201,8 @@ where
<div class={classes!(if props.button.is_some() { Some("input-group") } else { None })}>
<div class={classes!(if props.button.is_some() { Some("input-group") } else { None }, if success || error { Some("has-icon-right") } else { None })}>
<input id={id} class="form-input" type="file" accept={props.accept} multiple={props.multiple}
onchange={props.onchange.clone().reform(|x: ChangeData| {
match x {
ChangeData::Files(file_list) => file_list,
_ => unreachable!()
}
onchange={props.onchange.clone().reform(|x: Event| {
x.target_unchecked_into::<HtmlInputElement>().files().expect("this is a file input")
})}
/>
{
@ -312,7 +318,7 @@ where
pub placeholder: Option<String>,
pub default: Option<String>,
#[prop_or_default]
pub oninput: Callback<InputData>,
pub oninput: Callback<InputEvent>,
pub rows: Option<usize>,
pub cols: Option<usize>,
}
@ -327,8 +333,18 @@ where
let error = props.parsed.as_ref().map(|x| x.is_err()).unwrap_or(false);
let id = props.label.to_lowercase().replace(' ', "-");
// To properly set the default value, we need to force a second render
// so the noderef becomes valid.
let first_render = use_state(|| true);
let trigger = use_force_update();
let applied_default_value = use_state(|| false);
let node_ref = use_state(NodeRef::default);
let node_ref = use_node_ref();
if *first_render {
first_render.set(false);
trigger.force_update();
}
if let (false, Some(default), Some(input_element)) = (
*applied_default_value,
@ -346,7 +362,7 @@ where
</label>
<div class={classes!(if success || error { Some("has-icon-right") } else { None })}>
<textarea class="form-input" id={id} oninput={props.oninput.clone()}
ref={(*node_ref).clone()}
ref={node_ref}
placeholder={props.placeholder.as_ref().cloned()}
rows={props.rows.as_ref().map(ToString::to_string)}
cols={props.cols.as_ref().map(ToString::to_string)}
@ -405,6 +421,8 @@ pub struct ButtonProps {
pub icon: Option<VChild<Icon>>,
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
#[prop_or_default]
pub noderef: NodeRef,
}
#[function_component(Button)]
@ -420,6 +438,7 @@ pub fn button(props: &ButtonProps) -> Html {
)}
disabled={props.disabled}
onclick={props.onclick.clone()}
ref={props.noderef.clone()}
>
{ props.title.map(Into::into).unwrap_or_else(|| html!()) }
{ if props.icon.is_some() && props.title.is_some() { " " } else { "" } }
@ -442,6 +461,8 @@ pub struct HyperlinkButtonProps {
pub href: &'static str,
#[prop_or_default]
pub onclick: Callback<MouseEvent>,
#[prop_or_default]
pub noderef: NodeRef,
}
#[function_component(HyperlinkButton)]
@ -457,6 +478,7 @@ pub fn hyperlink_button(props: &HyperlinkButtonProps) -> Html {
disabled={props.disabled}
href={props.href}
onclick={props.onclick.clone()}
ref={props.noderef.clone()}
>
{ props.title.map(Into::into).unwrap_or_else(|| html!()) }
{ if props.icon.is_some() && props.title.is_some() { " " } else { "" } }

@ -1,3 +1,4 @@
use base64::Engine;
use std::path::Path;
use wasm_bindgen::JsCast;
use web_sys::{window, HtmlElement};
@ -6,13 +7,10 @@ pub fn prompt_download(path: impl AsRef<Path>, content: impl AsRef<[u8]>) {
let window = window().unwrap();
let document = window.document().unwrap();
let hyperlink = document.create_element("a").unwrap();
let content_base64 = base64::encode(content);
hyperlink
.set_attribute(
"href",
&format!("data:text/plain;base64,{}", content_base64),
)
.unwrap();
let mut href = "data:text/plain;base64,".to_string();
base64::engine::general_purpose::STANDARD_NO_PAD.encode_string(content, &mut href);
hyperlink.set_attribute("href", &href).unwrap();
hyperlink
.set_attribute("download", &path.as_ref().display().to_string())
.unwrap();

Loading…
Cancel
Save