From 7bd80191a0eeed5b4974e02ead8787242ca1b926 Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Fri, 13 Aug 2021 00:39:01 -0400 Subject: [PATCH] web: support multiple SVG file selection and g-code generation --- web/src/inputs.rs | 103 +++++++++++++++++++++------------ web/src/main.rs | 128 +++++++++++++++++++++-------------------- web/src/spectre/mod.rs | 94 +++++++++++++++++++++++++----- web/src/state.rs | 13 +++-- 4 files changed, 219 insertions(+), 119 deletions(-) diff --git a/web/src/inputs.rs b/web/src/inputs.rs index bcf5705..45832cb 100644 --- a/web/src/inputs.rs +++ b/web/src/inputs.rs @@ -4,7 +4,7 @@ use gloo_file::futures::read_as_text; use gloo_timers::callback::Timeout; use paste::paste; use roxmltree::Document; -use std::num::ParseFloatError; +use std::{num::ParseFloatError}; use web_sys::{FileList, HtmlElement}; use yew::prelude::*; use yewdux::prelude::{BasicStore, Dispatcher}; @@ -13,7 +13,7 @@ use yewdux_input::*; use crate::{ spectre::*, - state::{AppState, AppStore, FormState, FormStore}, + state::{AppState, AppStore, FormState, FormStore, Svg}, }; // TODO: make a nice, syntax highlighting editor for g-code. @@ -208,11 +208,13 @@ macro_rules! gcode_input { }) }; 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())} - oninput={oninput} - /> + + 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())} + oninput={oninput} + /> + } } } @@ -257,11 +259,13 @@ macro_rules! form_input { let form = use_store::>(); let oninput = form.dispatch().input(|state, value| state.$accessor $([$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())} - oninput={oninput} - /> + + 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())} + oninput={oninput} + /> + } } } @@ -499,7 +503,7 @@ pub fn settings_form() -> Html { #[function_component(SvgInput)] pub fn svg_input() -> Html { let app = use_store::(); - let parsed_state = use_state::>, _>(|| None); + let parsed_state = use_ref(|| vec![]); let parsed_state_cloned = parsed_state.clone(); @@ -508,39 +512,62 @@ pub fn svg_input() -> Html { .future_callback_with(move |app, file_list: FileList| { let parsed_state_cloned = parsed_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 parsed_state_cloned = parsed_state_cloned.clone(); - let svg_filename = file.name(); - let res = read_as_text(&gloo_file::File::from(file)).await; - app.reduce(move |app| { - let parsed = - Some(res.map_err(|err| err.to_string()).and_then(move |text| { + 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 SVG: {}", err)) + Err(format!("Error parsing {}: {}", &filename, err)) } else { - Ok(text) + Ok(Svg { + content: text, + filename, + }) } - })); - parsed_state_cloned.set(parsed.clone()); - app.svg = parsed.transpose().ok().clone().flatten(); - app.svg_filename = if app.svg.is_some() { - Some(svg_filename) - } else { - None - }; - }); + }), + ); } + app.reduce(move |app| { + app.svgs.clear(); + (*parsed_state_cloned).borrow_mut().clear(); + for result in results.iter() { + (*parsed_state_cloned) + .borrow_mut() + .push(result.clone().map(|_| ())); + } + if results.iter().all(Result::is_ok) { + app.svgs.extend(results.drain(..).filter_map(Result::ok)); + } + }); } }); - let parsed_cloned = (*parsed_state).clone(); + let errors = parsed_state + .borrow() + .iter() + .filter_map(|res| res.as_ref().err()) + .cloned() + .collect::>(); + let res = if parsed_state.borrow().is_empty() { + None + } else if errors.is_empty() { + Some(Ok(())) + } else { + Some(Err(errors.join("\n"))) + }; html! { - - label="Select an SVG" - accept=".svg" - multiple={false} - parsed={parsed_cloned} - onchange={onchange} - /> + + + label="Select SVG files" + accept=".svg" + multiple={true} + parsed={res} + onchange={onchange} + /> + } } diff --git a/web/src/main.rs b/web/src/main.rs index 1a976cf..7efb1f3 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -68,66 +68,70 @@ impl Component for App { // pull this out into one so that the UI can actually // show progress updates. self.link.send_future(async move { - let options = ConversionOptions { - tolerance: app_state.tolerance, - feedrate: app_state.feedrate, - dpi: app_state.dpi, - }; - let machine = Machine::new( - app_state - .tool_on_sequence - .as_ref() - .map(String::as_str) - .map(snippet_parser) - .transpose() - .unwrap(), - app_state - .tool_off_sequence - .as_ref() - .map(String::as_str) - .map(snippet_parser) - .transpose() - .unwrap(), - app_state - .begin_sequence - .as_ref() - .map(String::as_str) - .map(snippet_parser) - .transpose() - .unwrap(), - app_state - .end_sequence - .as_ref() - .map(String::as_str) - .map(snippet_parser) - .transpose() - .unwrap(), - ); - let document = Document::parse(app_state.svg.as_ref().unwrap()).unwrap(); - - let mut turtle = Turtle::new(machine); - let mut program = svg2program(&document, options, &mut turtle); - - set_origin(&mut program, app_state.origin); - - let gcode_base64 = { - let mut cursor = Cursor::new(vec![]); - tokens_into_gcode_bytes(&program, &mut cursor).unwrap(); - base64::encode(cursor.get_ref()) - }; - - let window = window(); - let document = window.document().unwrap(); - let hyperlink = document.create_element("a").unwrap(); - - let filepath = - Path::new(app_state.svg_filename.as_ref().unwrap()).with_extension("gcode"); - let filename = filepath.to_str().unwrap(); - hyperlink - .set_attribute("href", &format!("data:text/plain;base64,{}", gcode_base64)) - .unwrap(); - hyperlink.set_attribute("download", filename).unwrap(); - hyperlink.unchecked_into::().click(); + for svg in app_state.svgs.iter() { + let options = ConversionOptions { + tolerance: app_state.tolerance, + feedrate: app_state.feedrate, + dpi: app_state.dpi, + }; + let machine = Machine::new( + app_state + .tool_on_sequence + .as_ref() + .map(String::as_str) + .map(snippet_parser) + .transpose() + .unwrap(), + app_state + .tool_off_sequence + .as_ref() + .map(String::as_str) + .map(snippet_parser) + .transpose() + .unwrap(), + app_state + .begin_sequence + .as_ref() + .map(String::as_str) + .map(snippet_parser) + .transpose() + .unwrap(), + app_state + .end_sequence + .as_ref() + .map(String::as_str) + .map(snippet_parser) + .transpose() + .unwrap(), + ); + let document = Document::parse(svg.content.as_str()).unwrap(); + + let mut turtle = Turtle::new(machine); + let mut program = svg2program(&document, options, &mut turtle); + + set_origin(&mut program, app_state.origin); + + let gcode_base64 = { + let mut cursor = Cursor::new(vec![]); + tokens_into_gcode_bytes(&program, &mut cursor).unwrap(); + base64::encode(cursor.get_ref()) + }; + + let window = window(); + let document = window.document().unwrap(); + let hyperlink = document.create_element("a").unwrap(); + + let filepath = Path::new(svg.filename.as_str()).with_extension("gcode"); + let filename = filepath.to_str().unwrap(); + hyperlink + .set_attribute( + "href", + &format!("data:text/plain;base64,{}", gcode_base64), + ) + .unwrap(); + hyperlink.set_attribute("download", filename).unwrap(); + hyperlink.unchecked_into::().click(); + } AppMsg::Done }); @@ -145,7 +149,7 @@ impl Component for App { } fn view(&self) -> Html { - let generate_disabled = self.generating || self.app_state.svg.is_none(); + 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. @@ -168,7 +172,7 @@ impl Component for App {