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 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! {
<TextArea<String, String> 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}
/>
<FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).as_ref().map(Result::is_ok)).flatten()}>
<TextArea<String, String> 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}
/>
</FormGroup>
}
}
}
@ -257,11 +259,13 @@ macro_rules! form_input {
let form = use_store::<BasicStore<FormState>>();
let oninput = form.dispatch().input(|state, value| state.$accessor $([$idx])? = value.parse::<f64>());
html! {
<Input<f64, ParseFloatError> 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}
/>
<FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).is_ok())}>
<Input<f64, ParseFloatError> 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}
/>
</FormGroup>
}
}
}
@ -499,7 +503,7 @@ pub fn settings_form() -> Html {
#[function_component(SvgInput)]
pub fn svg_input() -> Html {
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();
@ -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::<Vec<_>>();
let res = if parsed_state.borrow().is_empty() {
None
} else if errors.is_empty() {
Some(Ok(()))
} else {
Some(Err(errors.join("\n")))
};
html! {
<FileUpload<String, String>
label="Select an SVG"
accept=".svg"
multiple={false}
parsed={parsed_cloned}
onchange={onchange}
/>
<FormGroup success={res.as_ref().map(Result::is_ok)}>
<FileUpload<(), String>
label="Select SVG files"
accept=".svg"
multiple={true}
parsed={res}
onchange={onchange}
/>
</FormGroup>
}
}

@ -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::<HtmlElement>().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::<HtmlElement>().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 {
<SvgInput/>
<ButtonGroup>
<Button
title="Generate GCode"
title="Generate g-code"
style={ButtonStyle::Primary}
loading={self.generating}
icon={

@ -81,10 +81,7 @@ where
}
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()}>
{ props.label }
</label>
@ -112,7 +109,7 @@ where
html!()
}
}
</div>
</>
}
}
@ -142,10 +139,7 @@ where
let error = props.parsed.as_ref().map(|x| x.is_err()).unwrap_or(false);
let id = props.label.to_lowercase().replace(' ', "-");
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()}>
{ props.label }
</label>
@ -178,6 +172,81 @@ where
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>
}
}
@ -222,10 +291,7 @@ where
}
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()}>
{ props.label }
</label>
@ -256,7 +322,7 @@ where
html!()
}
}
</div>
</>
}
}

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

Loading…
Cancel
Save