move import/export button to settings modal + reorg mods

master
Sameer Puri 4 years ago
parent 7019264735
commit 71abedb3b3

@ -0,0 +1,246 @@
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 yew::prelude::*;
use yewdux_functional::use_store;
use yewdux_input::*;
use crate::{
state::{AppState, AppStore, FormStore},
ui::{FormGroup, TextArea},
};
macro_rules! gcode_input {
($($name: ident {
$label: literal,
$desc: literal,
$form_accessor: expr $(=> $form_idx: literal)?,
$app_accessor: expr $(=> $app_idx: literal)?,
})*) => {
$(
paste! {
#[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 timeout = use_state::<Option<Timeout>, _>(|| None);
let oninput = {
let timeout = timeout.clone();
form.dispatch().input(move |state, value| {
let res = Some(match snippet_parser(&value) {
Ok(_) => Ok(value),
Err(err) => {
let mut buf = NoColor::new(vec![]);
let config = Config::default();
emit(
&mut buf,
&config,
&codespan_reporting::files::SimpleFile::new("<input>", value),
&into_diagnostic(&err),
)
.unwrap();
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)
});
let timeout_inner = timeout.clone();
timeout.set(Some(Timeout::new(VALIDATION_TIMEOUT, move || {
timeout_inner.set(None);
})));
state.$form_accessor $([$form_idx])? = res;
})
};
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).as_ref().map(Result::is_ok)).flatten()}>
<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())}
oninput={oninput}
/>
</FormGroup>
}
}
}
)*
};
}
gcode_input! {
ToolOnSequence {
"Tool On Sequence",
"G-Code for turning on the tool",
tool_on_sequence,
settings.machine.tool_on_sequence,
}
ToolOffSequence {
"Tool Off Sequence",
"G-Code for turning off the tool",
tool_off_sequence,
settings.machine.tool_off_sequence,
}
BeginSequence {
"Program Begin Sequence",
"G-Code for initializing the machine at the beginning of the program",
begin_sequence,
settings.machine.begin_sequence,
}
EndSequence {
"Program End Sequence",
"G-Code for stopping/idling the machine at the end of the program",
end_sequence,
settings.machine.end_sequence,
}
}
// TODO: make a nice, syntax highlighting editor for g-code.
// I started on this but it quickly got too complex.
// pub struct GCodeEditor {
// props: GCodeEditorProps,
// dispatch: AppDispatch,
// state: Rc<State>,
// validation_task: Option<TimeoutTask>,
// link: ComponentLink<Self>,
// parsed: Option<Result<Html, String>>,
// node_ref: NodeRef,
// }
// pub enum InputMessage {
// Validate(String),
// State(Rc<State>),
// Change(InputData),
// }
// impl Component for GCodeEditor {
// type Message = InputMessage;
// type Properties = GCodeEditorProps;
// fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
// Self {
// props,
// dispatch: Dispatch::bridge_state(link.callback(InputMessage::State)),
// state: Default::default(),
// validation_task: None,
// link,
// parsed: None,
// node_ref: NodeRef::default(),
// }
// }
// fn update(&mut self, msg: Self::Message) -> ShouldRender {
// match msg {
// InputMessage::State(state) => {
// self.state = state;
// true
// }
// InputMessage::Validate(value) => {
// self.parsed = Some(snippet_parser(&value).map(|snippet| {
// html! {
// <>
// {
// for snippet.iter_emit_tokens().flat_map(|token| {
// if let Token::Field(field) = &token {
// vec![
// html! {
// <span class=classes!("hljs-type")>{field.letters.to_string()}</span>
// },
// {
// let class = match &field.value {
// Value::Rational(_) | Value::Integer(_) | Value::Float(_) => "hljs-number",
// Value::String(_) => "hljs-string",
// };
// html! {
// <span class=classes!(class)>{field.value.to_string()}</span>
// }
// }
// ]
// } else if let Token::Newline{..} = &token {
// vec![
// html! {
// "\r\n"
// }
// ]
// }
// else {
// let class = match &token {
// Token::Comment{..} => "hljs-comment",
// Token::Checksum(..) => "hljs-number",
// Token::Whitespace(..) => "whitespace",
// Token::Newline{..} => "newline",
// Token::Percent => "hljs-keyword",
// _ => unreachable!(),
// };
// vec![html!{
// <span class=classes!("token", class)>
// { token.to_string() }
// </span>
// }]
// }
// })
// }
// </>
// }
// }).map_err(|err| {
// let mut buf = Buffer::no_color();
// let config = Config::default();
// emit(
// &mut buf,
// &config,
// &codespan_reporting::files::SimpleFile::new("<input>", value),
// &into_diagnostic(&err),
// )
// .unwrap();
// String::from_utf8_lossy(buf.as_slice()).to_string()
// }));
// true
// }
// InputMessage::Change(InputData { value, .. }) => {
// self.parsed = None;
// self.validation_task = None;
// self.validation_task = Some(TimeoutService::spawn(
// self.props.validation_timeout,
// self.link
// .callback(move |_| InputMessage::Validate(value.clone())),
// ));
// true
// }
// }
// }
// fn change(&mut self, props: Self::Properties) -> ShouldRender {
// self.props.neq_assign(props)
// }
// fn view(&self) -> Html {
// let oninput = self.link.callback(|x: InputData| InputMessage::Change(x));
// html! {
// <>
// <div class=classes!("editor-container")>
// <label>
// {self.props.label}
// <textarea class=classes!("editor") ref=self.node_ref.clone() oninput=oninput />
// </label>
// <br/>
// <pre class=classes!("hljs") ref=self.node_ref.clone() aria-hidden="true">
// {
// if let Some(res) = self.parsed.as_ref() {
// match res.as_ref() {
// Ok(parsed) => parsed.clone(),
// Err(err) => err.into()
// }
// } else {
// html! {}
// }
// }
// </pre>
// </div>
// </>
// }
// }
// }

@ -0,0 +1,73 @@
use paste::paste;
use std::num::ParseFloatError;
use yew::prelude::*;
use yewdux::prelude::{BasicStore};
use yewdux_functional::use_store;
use yewdux_input::*;
use crate::{
state::{AppState, AppStore, FormState},
ui::*,
};
macro_rules! form_input {
($($name: ident {
$label: literal,
$desc: literal,
$form_accessor: expr $(=> $form_idx: literal)?,
$app_accessor: expr $(=> $app_idx: literal)?,
})*) => {
$(
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>());
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).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())}
oninput={oninput}
/>
</FormGroup>
}
}
}
)*
};
}
form_input! {
Tolerance {
"Tolerance",
"Curve interpolation tolerance (mm)",
tolerance,
settings.conversion.tolerance,
}
Feedrate {
"Feedrate",
"Machine feedrate (mm/min)",
feedrate,
settings.conversion.feedrate,
}
Dpi {
"Dots per Inch",
"Used for scaling visual units (pixels, points, picas, etc.)",
dpi,
settings.conversion.dpi,
}
OriginX {
"Origin X",
"X-axis coordinate for the bottom left corner of the machine",
origin => 0,
settings.postprocess.origin => 0,
}
OriginY {
"Origin Y",
"Y-axis coordinate for the bottom left corner of the machine",
origin => 1,
settings.postprocess.origin => 1,
}
}

@ -1,11 +1,7 @@
use codespan_reporting::term::{emit, termcolor::NoColor, Config};
use g_code::parse::{into_diagnostic, snippet_parser};
use gloo_file::futures::{read_as_bytes, read_as_text}; use gloo_file::futures::{read_as_bytes, read_as_text};
use gloo_timers::callback::Timeout;
use js_sys::TypeError; use js_sys::TypeError;
use paste::paste;
use roxmltree::Document; use roxmltree::Document;
use std::{convert::TryInto, num::ParseFloatError, path::Path}; use std::{convert::TryInto, path::Path};
use svg2gcode::Settings; use svg2gcode::Settings;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
@ -13,308 +9,20 @@ use web_sys::{window, FileList, HtmlElement, Response};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::{BasicStore, Dispatcher}; use yewdux::prelude::{BasicStore, Dispatcher};
use yewdux_functional::use_store; use yewdux_functional::use_store;
use yewdux_input::*;
use crate::{ use crate::{
spectre::*, state::{AppStore, FormState, Svg},
state::{AppState, AppStore, FormState, FormStore, Svg}, ui::{
Button, ButtonStyle, Checkbox, FileUpload, FormGroup, HyperlinkButton, Icon, IconName,
Input, InputType, Modal,
},
}; };
// TODO: make a nice, syntax highlighting editor for g-code. mod editors;
// I started on this but it quickly got too complex. mod inputs;
// pub struct GCodeEditor {
// props: GCodeEditorProps,
// dispatch: AppDispatch,
// state: Rc<State>,
// validation_task: Option<TimeoutTask>,
// link: ComponentLink<Self>,
// parsed: Option<Result<Html, String>>,
// node_ref: NodeRef,
// }
// pub enum InputMessage { use editors::*;
// Validate(String), use inputs::*;
// State(Rc<State>),
// Change(InputData),
// }
// impl Component for GCodeEditor {
// type Message = InputMessage;
// type Properties = GCodeEditorProps;
// fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
// Self {
// props,
// dispatch: Dispatch::bridge_state(link.callback(InputMessage::State)),
// state: Default::default(),
// validation_task: None,
// link,
// parsed: None,
// node_ref: NodeRef::default(),
// }
// }
// fn update(&mut self, msg: Self::Message) -> ShouldRender {
// match msg {
// InputMessage::State(state) => {
// self.state = state;
// true
// }
// InputMessage::Validate(value) => {
// self.parsed = Some(snippet_parser(&value).map(|snippet| {
// html! {
// <>
// {
// for snippet.iter_emit_tokens().flat_map(|token| {
// if let Token::Field(field) = &token {
// vec![
// html! {
// <span class=classes!("hljs-type")>{field.letters.to_string()}</span>
// },
// {
// let class = match &field.value {
// Value::Rational(_) | Value::Integer(_) | Value::Float(_) => "hljs-number",
// Value::String(_) => "hljs-string",
// };
// html! {
// <span class=classes!(class)>{field.value.to_string()}</span>
// }
// }
// ]
// } else if let Token::Newline{..} = &token {
// vec![
// html! {
// "\r\n"
// }
// ]
// }
// else {
// let class = match &token {
// Token::Comment{..} => "hljs-comment",
// Token::Checksum(..) => "hljs-number",
// Token::Whitespace(..) => "whitespace",
// Token::Newline{..} => "newline",
// Token::Percent => "hljs-keyword",
// _ => unreachable!(),
// };
// vec![html!{
// <span class=classes!("token", class)>
// { token.to_string() }
// </span>
// }]
// }
// })
// }
// </>
// }
// }).map_err(|err| {
// let mut buf = Buffer::no_color();
// let config = Config::default();
// emit(
// &mut buf,
// &config,
// &codespan_reporting::files::SimpleFile::new("<input>", value),
// &into_diagnostic(&err),
// )
// .unwrap();
// String::from_utf8_lossy(buf.as_slice()).to_string()
// }));
// true
// }
// InputMessage::Change(InputData { value, .. }) => {
// self.parsed = None;
// self.validation_task = None;
// self.validation_task = Some(TimeoutService::spawn(
// self.props.validation_timeout,
// self.link
// .callback(move |_| InputMessage::Validate(value.clone())),
// ));
// true
// }
// }
// }
// fn change(&mut self, props: Self::Properties) -> ShouldRender {
// self.props.neq_assign(props)
// }
// fn view(&self) -> Html {
// let oninput = self.link.callback(|x: InputData| InputMessage::Change(x));
// html! {
// <>
// <div class=classes!("editor-container")>
// <label>
// {self.props.label}
// <textarea class=classes!("editor") ref=self.node_ref.clone() oninput=oninput />
// </label>
// <br/>
// <pre class=classes!("hljs") ref=self.node_ref.clone() aria-hidden="true">
// {
// if let Some(res) = self.parsed.as_ref() {
// match res.as_ref() {
// Ok(parsed) => parsed.clone(),
// Err(err) => err.into()
// }
// } else {
// html! {}
// }
// }
// </pre>
// </div>
// </>
// }
// }
// }
macro_rules! gcode_input {
($($name: ident {
$label: literal,
$desc: literal,
$form_accessor: expr $(=> $form_idx: literal)?,
$app_accessor: expr $(=> $app_idx: literal)?,
})*) => {
$(
paste! {
#[function_component([<$name Input>])]
fn [<$name:snake:lower _input>]() -> Html {
const VALIDATION_TIMEOUT: u32 = 350;
let app = use_store::<AppStore>();
let form = use_store::<FormStore>();
let timeout = use_state::<Option<Timeout>, _>(|| None);
let oninput = {
let timeout = timeout.clone();
form.dispatch().input(move |state, value| {
let res = Some(match snippet_parser(&value) {
Ok(_) => Ok(value),
Err(err) => {
let mut buf = NoColor::new(vec![]);
let config = Config::default();
emit(
&mut buf,
&config,
&codespan_reporting::files::SimpleFile::new("<input>", value),
&into_diagnostic(&err),
)
.unwrap();
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)
});
let timeout_inner = timeout.clone();
timeout.set(Some(Timeout::new(VALIDATION_TIMEOUT, move || {
timeout_inner.set(None);
})));
state.$form_accessor $([$form_idx])? = res;
})
};
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).as_ref().map(Result::is_ok)).flatten()}>
<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())}
oninput={oninput}
/>
</FormGroup>
}
}
}
)*
};
}
gcode_input! {
ToolOnSequence {
"Tool On Sequence",
"G-Code for turning on the tool",
tool_on_sequence,
settings.machine.tool_on_sequence,
}
ToolOffSequence {
"Tool Off Sequence",
"G-Code for turning off the tool",
tool_off_sequence,
settings.machine.tool_off_sequence,
}
BeginSequence {
"Program Begin Sequence",
"G-Code for initializing the machine at the beginning of the program",
begin_sequence,
settings.machine.begin_sequence,
}
EndSequence {
"Program End Sequence",
"G-Code for stopping/idling the machine at the end of the program",
end_sequence,
settings.machine.end_sequence,
}
}
macro_rules! form_input {
($($name: ident {
$label: literal,
$desc: literal,
$form_accessor: expr $(=> $form_idx: literal)?,
$app_accessor: expr $(=> $app_idx: literal)?,
})*) => {
$(
paste! {
#[function_component([<$name Input>])]
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>());
html! {
<FormGroup success={form.state().map(|state| (state.$form_accessor $([$form_idx])?).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())}
oninput={oninput}
/>
</FormGroup>
}
}
}
)*
};
}
form_input! {
Tolerance {
"Tolerance",
"Curve interpolation tolerance (mm)",
tolerance,
settings.conversion.tolerance,
}
Feedrate {
"Feedrate",
"Machine feedrate (mm/min)",
feedrate,
settings.conversion.feedrate,
}
Dpi {
"Dots per Inch",
"Used for scaling visual units (pixels, points, picas, etc.)",
dpi,
settings.conversion.dpi,
}
OriginX {
"Origin X",
"X-axis coordinate for the bottom left corner of the machine",
origin => 0,
settings.postprocess.origin => 0,
}
OriginY {
"Origin Y",
"Y-axis coordinate for the bottom left corner of the machine",
origin => 1,
settings.postprocess.origin => 1,
}
}
#[function_component(SettingsForm)] #[function_component(SettingsForm)]
pub fn settings_form() -> Html { pub fn settings_form() -> Html {
@ -422,18 +130,26 @@ pub fn settings_form() -> Html {
footer={ footer={
html!( html!(
<> <>
<HyperlinkButton
title="Import/Export"
href="#import_export"
style={ButtonStyle::Default}
disabled={disabled}
icon={IconName::Copy}
/>
{" "}
<Button <Button
title="Save"
style={ButtonStyle::Primary} style={ButtonStyle::Primary}
disabled={disabled} disabled={disabled}
title="Save"
onclick={save_onclick} onclick={save_onclick}
/> />
{" "} {" "}
<HyperlinkButton <HyperlinkButton
ref={close_ref} ref={close_ref}
style={ButtonStyle::Default}
title="Close" title="Close"
href="#close" href="#close"
style={ButtonStyle::Default}
/> />
</> </>
) )
@ -447,16 +163,21 @@ pub fn import_export_modal() -> Html {
let app = use_store::<AppStore>(); let app = use_store::<AppStore>();
let import_state = use_state(|| Option::<Result<Settings, String>>::None); let import_state = use_state(|| Option::<Result<Settings, String>>::None);
let export_onclick = let export_error = use_state(|| Option::<String>::None);
let export_onclick = {
let export_error = export_error.clone();
app.dispatch() app.dispatch()
.reduce_callback(|app| match serde_json::to_vec_pretty(&app.settings) { .reduce_callback(move |app| match serde_json::to_vec_pretty(&app.settings) {
Ok(settings_json_bytes) => { Ok(settings_json_bytes) => {
let filename = format!("svg2gcode_settings"); let filename = format!("svg2gcode_settings");
let filepath = Path::new(&filename).with_extension("json"); 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) => {} Err(serde_json_err) => {
}); export_error.set(Some(serde_json_err.to_string()));
}
})
};
let settings_upload_onchange = { let settings_upload_onchange = {
let import_state = import_state.clone(); let import_state = import_state.clone();
@ -538,6 +259,15 @@ pub fn import_export_modal() -> Html {
icon={html_nested!(<Icon name={IconName::Download}/>)} icon={html_nested!(<Icon name={IconName::Download}/>)}
onclick={export_onclick} onclick={export_onclick}
/> />
{
if let Some(ref err) = *export_error {
html!{
<pre class="text-error">{ err }</pre>
}
} else {
html!{}
}
}
</> </>
) )
} }
@ -555,8 +285,8 @@ pub fn import_export_modal() -> Html {
} }
} }
#[function_component(SvgInput)] #[function_component(SvgForm)]
pub fn svg_input() -> Html { pub fn svg_form() -> Html {
let app = use_store::<AppStore>(); let app = use_store::<AppStore>();
let file_upload_state = use_ref(Vec::default); let file_upload_state = use_ref(Vec::default);

@ -10,13 +10,13 @@ use svg2gcode::{set_origin, svg2program, ConversionOptions, Machine, Turtle};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::{Dispatch, Dispatcher}; use yewdux::prelude::{Dispatch, Dispatcher};
mod inputs; mod forms;
mod spectre; mod ui;
mod state; mod state;
mod util; mod util;
use inputs::*; use forms::*;
use spectre::*; use ui::*;
use state::*; use state::*;
use util::*; use util::*;
@ -167,7 +167,7 @@ impl Component for App {
<p> <p>
{ env!("CARGO_PKG_DESCRIPTION") } { env!("CARGO_PKG_DESCRIPTION") }
</p> </p>
<SvgInput/> <SvgForm/>
<div class={classes!("card-container", "columns")}> <div class={classes!("card-container", "columns")}>
{ {
for self.app_state.svgs.iter().enumerate().map(|(i, svg)| { for self.app_state.svgs.iter().enumerate().map(|(i, svg)| {
@ -221,12 +221,6 @@ impl Component for App {
onclick={settings_hydrate_onclick} onclick={settings_hydrate_onclick}
href="#settings" href="#settings"
/> />
<HyperlinkButton
title="Import/Export"
style={ButtonStyle::Default}
icon={IconName::Copy}
href="#import_export"
/>
</ButtonGroup> </ButtonGroup>
<SettingsForm/> <SettingsForm/>
<ImportExportModal/> <ImportExportModal/>

Loading…
Cancel
Save