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, app_state: Rc, form_dispatch: Dispatch, form_state: Rc, generating: bool, link: ComponentLink, } enum AppMsg { AppState(Rc), FormState(Rc), Generate, Done, } impl Component for App { type Message = AppMsg; type Properties = (); fn create(_props: Self::Properties, link: ComponentLink) -> 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! {

{ "svg2gcode" }

{ env!("CARGO_PKG_DESCRIPTION") }

{ 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!{

{ "See the project " } { "on GitHub" } {" for support" }

} } } fn main() { wasm_logger::init(wasm_logger::Config::new(Level::Info)); yew::start_app::(); }