You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
220 lines
7.5 KiB
220 lines
7.5 KiB
use std::{io::Cursor, path::Path, rc::Rc};
|
|
|
|
use g_code::parse::snippet_parser;
|
|
use log::Level;
|
|
use roxmltree::Document;
|
|
use svg2gcode::{
|
|
set_origin, svg2program, tokens_into_gcode_bytes, ConversionOptions, Machine, Turtle,
|
|
};
|
|
use wasm_bindgen::JsCast;
|
|
use web_sys::HtmlElement;
|
|
use yew::{prelude::*, utils::window};
|
|
use yewdux::prelude::{Dispatch, Dispatcher};
|
|
|
|
mod inputs;
|
|
mod spectre;
|
|
mod state;
|
|
|
|
use inputs::*;
|
|
use spectre::*;
|
|
use state::*;
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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();
|
|
|
|
AppMsg::Done
|
|
});
|
|
true
|
|
}
|
|
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.svg.is_none();
|
|
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.as_ref());
|
|
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>
|
|
<SvgInput/>
|
|
<ButtonGroup>
|
|
<Button
|
|
title="Generate GCode"
|
|
style={ButtonStyle::Primary}
|
|
loading={self.generating}
|
|
icon={
|
|
html_nested! (
|
|
<Icon name={IconName::Download} />
|
|
)
|
|
}
|
|
disabled={generate_disabled}
|
|
onclick={generate_onclick}
|
|
/>
|
|
<HyperlinkButton
|
|
title="Settings"
|
|
style={ButtonStyle::Default}
|
|
icon={IconName::Edit}
|
|
onclick={settings_hydrate_onclick}
|
|
href="#settings"
|
|
/>
|
|
</ButtonGroup>
|
|
<SettingsForm />
|
|
</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>
|
|
}
|
|
}
|
|
}
|
|
|
|
// #[function_component(App)]
|
|
// fn app() -> Html {
|
|
// let app = use_store::<AppStore>();
|
|
// let form = use_store::<BasicStore<FormState>>();
|
|
|
|
// let generating = use_state(|| false);
|
|
|
|
// }
|
|
// }
|
|
|
|
fn main() {
|
|
wasm_logger::init(wasm_logger::Config::new(Level::Info));
|
|
yew::start_app::<App>();
|
|
}
|