web: support multiple SVG file selection and g-code generation

master
Sameer Puri 4 years ago
parent 862919a9c2
commit 7bd80191a0

@ -4,7 +4,7 @@ use gloo_file::futures::read_as_text;
use gloo_timers::callback::Timeout; use gloo_timers::callback::Timeout;
use paste::paste; use paste::paste;
use roxmltree::Document; use roxmltree::Document;
use std::num::ParseFloatError; use std::{num::ParseFloatError};
use web_sys::{FileList, HtmlElement}; use web_sys::{FileList, HtmlElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::{BasicStore, Dispatcher}; use yewdux::prelude::{BasicStore, Dispatcher};
@ -13,7 +13,7 @@ use yewdux_input::*;
use crate::{ use crate::{
spectre::*, spectre::*,
state::{AppState, AppStore, FormState, FormStore}, state::{AppState, AppStore, FormState, FormStore, Svg},
}; };
// TODO: make a nice, syntax highlighting editor for g-code. // TODO: make a nice, syntax highlighting editor for g-code.
@ -208,11 +208,13 @@ macro_rules! gcode_input {
}) })
}; };
html! { html! {
<TextArea<String, String> label=$label desc=$desc <FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).as_ref().map(Result::is_ok)).flatten()}>
default={app.state().map(|state| (state.$accessor $([$idx])?).clone()).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)} <TextArea<String, String> label=$label desc=$desc
parsed={form.state().and_then(|state| (state.$accessor $([$idx])?).clone()).filter(|_| timeout.is_none())} default={app.state().map(|state| (state.$accessor $([$idx])?).clone()).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)}
oninput={oninput} parsed={form.state().and_then(|state| (state.$accessor $([$idx])?).clone()).filter(|_| timeout.is_none())}
/> oninput={oninput}
/>
</FormGroup>
} }
} }
} }
@ -257,11 +259,13 @@ macro_rules! form_input {
let form = use_store::<BasicStore<FormState>>(); let form = use_store::<BasicStore<FormState>>();
let oninput = form.dispatch().input(|state, value| state.$accessor $([$idx])? = value.parse::<f64>()); let oninput = form.dispatch().input(|state, value| state.$accessor $([$idx])? = value.parse::<f64>());
html! { html! {
<Input<f64, ParseFloatError> label=$label desc=$desc <FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).is_ok())}>
default={app.state().map(|state| state.$accessor $([$idx])?).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)} <Input<f64, ParseFloatError> label=$label desc=$desc
parsed={form.state().map(|state| (state.$accessor $([$idx])?).clone())} default={app.state().map(|state| state.$accessor $([$idx])?).unwrap_or_else(|| AppState::default().$accessor $([$idx])?)}
oninput={oninput} parsed={form.state().map(|state| (state.$accessor $([$idx])?).clone())}
/> oninput={oninput}
/>
</FormGroup>
} }
} }
} }
@ -499,7 +503,7 @@ pub fn settings_form() -> Html {
#[function_component(SvgInput)] #[function_component(SvgInput)]
pub fn svg_input() -> Html { pub fn svg_input() -> Html {
let app = use_store::<AppStore>(); let app = use_store::<AppStore>();
let parsed_state = use_state::<Option<Result<String, String>>, _>(|| None); let parsed_state = use_ref(|| vec![]);
let parsed_state_cloned = parsed_state.clone(); let parsed_state_cloned = parsed_state.clone();
@ -508,39 +512,62 @@ pub fn svg_input() -> Html {
.future_callback_with(move |app, file_list: FileList| { .future_callback_with(move |app, file_list: FileList| {
let parsed_state_cloned = parsed_state_cloned.clone(); let parsed_state_cloned = parsed_state_cloned.clone();
async move { 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)) { for file in (0..file_list.length()).filter_map(|i| file_list.item(i)) {
let parsed_state_cloned = parsed_state_cloned.clone(); let filename = file.name();
let svg_filename = file.name(); results.push(
let res = read_as_text(&gloo_file::File::from(file)).await; read_as_text(&gloo_file::File::from(file))
app.reduce(move |app| { .await
let parsed = .map_err(|err| err.to_string())
Some(res.map_err(|err| err.to_string()).and_then(move |text| { .and_then(|text| {
if let Some(err) = Document::parse(&text).err() { if let Some(err) = Document::parse(&text).err() {
Err(format!("Error parsing SVG: {}", err)) Err(format!("Error parsing {}: {}", &filename, err))
} else { } 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::<Vec<_>>();
let res = if parsed_state.borrow().is_empty() {
None
} else if errors.is_empty() {
Some(Ok(()))
} else {
Some(Err(errors.join("\n")))
};
html! { html! {
<FileUpload<String, String> <FormGroup success={res.as_ref().map(Result::is_ok)}>
label="Select an SVG" <FileUpload<(), String>
accept=".svg" label="Select SVG files"
multiple={false} accept=".svg"
parsed={parsed_cloned} multiple={true}
onchange={onchange} parsed={res}
/> onchange={onchange}
/>
</FormGroup>
} }
} }

@ -68,66 +68,70 @@ impl Component for App {
// pull this out into one so that the UI can actually // pull this out into one so that the UI can actually
// show progress updates. // show progress updates.
self.link.send_future(async move { self.link.send_future(async move {
let options = ConversionOptions { for svg in app_state.svgs.iter() {
tolerance: app_state.tolerance, let options = ConversionOptions {
feedrate: app_state.feedrate, tolerance: app_state.tolerance,
dpi: app_state.dpi, feedrate: app_state.feedrate,
}; dpi: app_state.dpi,
let machine = Machine::new( };
app_state let machine = Machine::new(
.tool_on_sequence app_state
.as_ref() .tool_on_sequence
.map(String::as_str) .as_ref()
.map(snippet_parser) .map(String::as_str)
.transpose() .map(snippet_parser)
.unwrap(), .transpose()
app_state .unwrap(),
.tool_off_sequence app_state
.as_ref() .tool_off_sequence
.map(String::as_str) .as_ref()
.map(snippet_parser) .map(String::as_str)
.transpose() .map(snippet_parser)
.unwrap(), .transpose()
app_state .unwrap(),
.begin_sequence app_state
.as_ref() .begin_sequence
.map(String::as_str) .as_ref()
.map(snippet_parser) .map(String::as_str)
.transpose() .map(snippet_parser)
.unwrap(), .transpose()
app_state .unwrap(),
.end_sequence app_state
.as_ref() .end_sequence
.map(String::as_str) .as_ref()
.map(snippet_parser) .map(String::as_str)
.transpose() .map(snippet_parser)
.unwrap(), .transpose()
); .unwrap(),
let document = Document::parse(app_state.svg.as_ref().unwrap()).unwrap(); );
let document = Document::parse(svg.content.as_str()).unwrap();
let mut turtle = Turtle::new(machine);
let mut program = svg2program(&document, options, &mut turtle); let mut turtle = Turtle::new(machine);
let mut program = svg2program(&document, options, &mut turtle);
set_origin(&mut program, app_state.origin);
set_origin(&mut program, app_state.origin);
let gcode_base64 = {
let mut cursor = Cursor::new(vec![]); let gcode_base64 = {
tokens_into_gcode_bytes(&program, &mut cursor).unwrap(); let mut cursor = Cursor::new(vec![]);
base64::encode(cursor.get_ref()) tokens_into_gcode_bytes(&program, &mut cursor).unwrap();
}; base64::encode(cursor.get_ref())
};
let window = window();
let document = window.document().unwrap(); let window = window();
let hyperlink = document.create_element("a").unwrap(); 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 filepath = Path::new(svg.filename.as_str()).with_extension("gcode");
let filename = filepath.to_str().unwrap(); let filename = filepath.to_str().unwrap();
hyperlink hyperlink
.set_attribute("href", &format!("data:text/plain;base64,{}", gcode_base64)) .set_attribute(
.unwrap(); "href",
hyperlink.set_attribute("download", filename).unwrap(); &format!("data:text/plain;base64,{}", gcode_base64),
hyperlink.unchecked_into::<HtmlElement>().click(); )
.unwrap();
hyperlink.set_attribute("download", filename).unwrap();
hyperlink.unchecked_into::<HtmlElement>().click();
}
AppMsg::Done AppMsg::Done
}); });
@ -145,7 +149,7 @@ impl Component for App {
} }
fn view(&self) -> Html { 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); let generate_onclick = self.link.callback(|_| AppMsg::Generate);
// TODO: come up with a less awkward way to do this. // TODO: come up with a less awkward way to do this.
@ -168,7 +172,7 @@ impl Component for App {
<SvgInput/> <SvgInput/>
<ButtonGroup> <ButtonGroup>
<Button <Button
title="Generate GCode" title="Generate g-code"
style={ButtonStyle::Primary} style={ButtonStyle::Primary}
loading={self.generating} loading={self.generating}
icon={ icon={

@ -81,10 +81,7 @@ where
} }
html! { html! {
<div class={classes!( <>
"form-group",
if success { Some("has-success") } else if error { Some("has-error") } else { None }
)}>
<label class="form-label" for={id.clone()}> <label class="form-label" for={id.clone()}>
{ props.label } { props.label }
</label> </label>
@ -112,7 +109,7 @@ where
html!() html!()
} }
} }
</div> </>
} }
} }
@ -142,10 +139,7 @@ where
let error = props.parsed.as_ref().map(|x| x.is_err()).unwrap_or(false); let error = props.parsed.as_ref().map(|x| x.is_err()).unwrap_or(false);
let id = props.label.to_lowercase().replace(' ', "-"); let id = props.label.to_lowercase().replace(' ', "-");
html! { html! {
<div class={classes!( <>
"form-group",
if success { Some("has-success") } else if error { Some("has-error") } else { None }
)}>
<label class="form-label" for={id.clone()}> <label class="form-label" for={id.clone()}>
{ props.label } { props.label }
</label> </label>
@ -178,6 +172,81 @@ where
html!() html!()
} }
} }
</>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct SelectProps {
#[prop_or_default]
pub children: Children,
#[prop_or(false)]
pub disabled: bool,
#[prop_or(false)]
pub multiple: bool,
}
#[function_component(Select)]
pub fn select(props: &SelectProps) -> Html {
html! {
<select class={classes!("form-select")}>{ for props.children.iter() }</select>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct OptionProps {
#[prop_or_default]
pub children: Children,
#[prop_or(false)]
pub selected: bool,
#[prop_or(false)]
pub disabled: bool,
pub value: Option<&'static str>,
}
#[function_component(Opt)]
pub fn option(props: &OptionProps) -> Html {
html! {
<option value={props.value}>{ for props.children.iter() }</option>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct InputGroupProps {
#[prop_or_default]
pub children: Children,
}
#[function_component(InputGroup)]
pub fn input_group(props: &InputGroupProps) -> Html {
html! {
<div class="input-group">
{ for props.children.iter() }
</div>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct FormGroupProps {
#[prop_or_default]
pub children: Children,
pub success: Option<bool>,
}
#[function_component(FormGroup)]
pub fn form_group(props: &FormGroupProps) -> Html {
html! {
<div class={classes!(
"form-group",
if let Some(true) = props.success {
Some("has-success")
} else if let Some(false) = props.success {
Some("has-error")
} else {
None
}
)}>
{ for props.children.iter() }
</div> </div>
} }
} }
@ -222,10 +291,7 @@ where
} }
html! { html! {
<div class={classes!( <>
"form-group",
if success { Some("has-success") } else if error { Some("has-error") } else { None }
)}>
<label class="form-label" for={id.clone()}> <label class="form-label" for={id.clone()}>
{ props.label } { props.label }
</label> </label>
@ -256,7 +322,7 @@ where
html!() html!()
} }
} }
</div> </>
} }
} }

@ -50,9 +50,13 @@ pub struct AppState {
pub end_sequence: Option<String>, pub end_sequence: Option<String>,
pub origin: [f64; 2], pub origin: [f64; 2],
#[serde(skip)] #[serde(skip)]
pub svg_filename: Option<String>, pub svgs: Vec<Svg>,
#[serde(skip)] }
pub svg: Option<String>,
#[derive(Debug, Clone)]
pub struct Svg {
pub content: String,
pub filename: String,
} }
impl Default for AppState { impl Default for AppState {
@ -67,8 +71,7 @@ impl Default for AppState {
begin_sequence: None, begin_sequence: None,
end_sequence: None, end_sequence: None,
origin: [0., 0.], origin: [0., 0.],
svg_filename: None, svgs: vec![],
svg: None,
} }
} }
} }

Loading…
Cancel
Save