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.
246 lines
9.2 KiB
246 lines
9.2 KiB
use std::{path::Path, rc::Rc};
|
|
|
|
use g_code::{
|
|
emit::{format_gcode_fmt, FormatOptions},
|
|
parse::snippet_parser,
|
|
};
|
|
use log::Level;
|
|
use roxmltree::Document;
|
|
use svg2gcode::{set_origin, svg2program, ConversionOptions, Machine, Turtle};
|
|
use yew::prelude::*;
|
|
use yewdux::prelude::{Dispatch, Dispatcher};
|
|
|
|
mod forms;
|
|
mod ui;
|
|
mod state;
|
|
mod util;
|
|
|
|
use forms::*;
|
|
use ui::*;
|
|
use state::*;
|
|
use util::*;
|
|
|
|
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 {
|
|
for svg in app_state.svgs.iter() {
|
|
let options = ConversionOptions {
|
|
dimensions: svg.dimensions,
|
|
};
|
|
|
|
let machine = Machine::new(
|
|
app_state.settings.machine.supported_functionality.clone(),
|
|
app_state
|
|
.settings
|
|
.machine
|
|
.tool_on_sequence
|
|
.as_deref()
|
|
.map(snippet_parser)
|
|
.transpose()
|
|
.unwrap(),
|
|
app_state
|
|
.settings
|
|
.machine
|
|
.tool_off_sequence
|
|
.as_deref()
|
|
.map(snippet_parser)
|
|
.transpose()
|
|
.unwrap(),
|
|
app_state
|
|
.settings
|
|
.machine
|
|
.begin_sequence
|
|
.as_deref()
|
|
.map(snippet_parser)
|
|
.transpose()
|
|
.unwrap(),
|
|
app_state
|
|
.settings
|
|
.machine
|
|
.end_sequence
|
|
.as_deref()
|
|
.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,
|
|
&app_state.settings.conversion,
|
|
options,
|
|
&mut turtle,
|
|
);
|
|
|
|
set_origin(&mut program, app_state.settings.postprocess.origin);
|
|
|
|
let gcode = {
|
|
let mut acc = String::new();
|
|
format_gcode_fmt(&program, FormatOptions::default(), &mut acc).unwrap();
|
|
acc
|
|
};
|
|
|
|
let filepath = Path::new(svg.filename.as_str()).with_extension("gcode");
|
|
prompt_download(filepath, &gcode.as_bytes());
|
|
}
|
|
|
|
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.svgs.is_empty();
|
|
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.settings);
|
|
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>
|
|
<SvgForm/>
|
|
<div class={classes!("card-container", "columns")}>
|
|
{
|
|
for self.app_state.svgs.iter().enumerate().map(|(i, svg)| {
|
|
let svg_base64 = base64::encode(svg.content.as_bytes());
|
|
let remove_svg_onclick = self.app_dispatch.reduce_callback_once(move |app| {
|
|
app.svgs.remove(i);
|
|
});
|
|
let footer = html!{
|
|
<Button
|
|
title="Remove"
|
|
style={ButtonStyle::Primary}
|
|
icon={
|
|
html_nested!(
|
|
<Icon name={IconName::Delete} />
|
|
)
|
|
}
|
|
onclick={remove_svg_onclick}
|
|
/>
|
|
};
|
|
html!{
|
|
<div class={classes!("column", "col-6", "col-xs-12")}>
|
|
<Card
|
|
title={svg.filename.clone()}
|
|
img={html_nested!(
|
|
<img class="img-responsive" src={format!("data:image/svg+xml;base64,{}", svg_base64)} alt={svg.filename.clone()} />
|
|
)}
|
|
footer={footer}
|
|
/>
|
|
</div>
|
|
}
|
|
})
|
|
}
|
|
</div>
|
|
<ButtonGroup>
|
|
<Button
|
|
title="Generate G-Code"
|
|
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/>
|
|
<ImportExportModal/>
|
|
</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>
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
wasm_logger::init(wasm_logger::Config::new(Level::Info));
|
|
yew::start_app::<App>();
|
|
}
|