// SPDX-FileCopyrightText: 2024 Topola contributors // // SPDX-License-Identifier: MIT use std::{ ops::ControlFlow, sync::mpsc::{channel, Receiver, Sender}, }; use topola::{ autorouter::{execution::Command, history::History}, board::edit::BoardEdit, interactor::{ activity::{InteractiveEvent, InteractiveEventKind, InteractiveInput}, Interactor, }, layout::via::ViaWeight, math::Circle, specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata}, }; use crate::{ appearance_panel::AppearancePanel, error_dialog::ErrorDialog, menu_bar::MenuBar, overlay::Overlay, translator::Translator, }; /// A loaded design and associated structures. pub struct Workspace { pub design: SpecctraDesign, pub appearance_panel: AppearancePanel, pub overlay: Overlay, pub interactor: Interactor, pub history_channel: ( Sender>>, Receiver>>, ), update_counter: f32, } impl Workspace { pub fn new(design: SpecctraDesign, tr: &Translator) -> Result { let board = design.make_board(&mut BoardEdit::new()); let appearance_panel = AppearancePanel::new(&board); let overlay = Overlay::new(&board).map_err(|err| { format!( "{}; {}", tr.text("tr-error-unable-to-initialize-overlay"), err ) })?; Ok(Self { design, appearance_panel, overlay, interactor: Interactor::new(board).map_err(|err| { format!( "{}; {}", tr.text("tr-error-unable-to-initialize-overlay"), err ) })?, history_channel: channel(), update_counter: 0.0, }) } /// Advances the app's state by the delta time `dt`. May call /// `.update_state()` more than once if the delta time is more than a multiple of /// the timestep. pub fn advance_state_by_dt( &mut self, tr: &Translator, error_dialog: &mut ErrorDialog, frame_timestep: f32, interactive_input: &InteractiveInput, ) { self.update_counter += interactive_input.dt; while self.update_counter >= frame_timestep { self.update_counter -= frame_timestep; if let ControlFlow::Break(()) = self.update_state(tr, error_dialog, interactive_input) { break; } } } pub fn update_state_for_event( &mut self, tr: &Translator, error_dialog: &mut ErrorDialog, menu_bar: &MenuBar, interactive_input: &InteractiveInput, interactive_event: InteractiveEvent, ) { if !self .interactor .maybe_activity() .as_ref() .map_or(true, |activity| { matches!(activity.maybe_status(), Some(ControlFlow::Break(..))) }) { match self .interactor .update_for_event(interactive_input, interactive_event) { ControlFlow::Continue(()) | ControlFlow::Break(Ok(())) => {} ControlFlow::Break(Err(err)) => { error_dialog.push_error("tr-module-invoker", format!("{}", err)); } } } else { match interactive_event.kind { InteractiveEventKind::PointerPrimaryButtonClicked => { if menu_bar.is_placing_via { self.interactor.execute(Command::PlaceVia(ViaWeight { from_layer: 0, to_layer: 0, circle: Circle { pos: interactive_input.pointer_pos, r: menu_bar.autorouter_options.router_options.routed_band_width / 2.0, }, maybe_net: Some(1234), })); } else { self.overlay.click( self.interactor.invoker().autorouter(), &self.appearance_panel, interactive_input.pointer_pos, ); } } InteractiveEventKind::PointerPrimaryButtonDragStarted => { self.overlay.drag_start( self.interactor.invoker().autorouter(), &self.appearance_panel, interactive_input.pointer_pos, interactive_event.ctrl, interactive_event.shift, ); } InteractiveEventKind::PointerPrimaryButtonDragStopped => { self.overlay.drag_stop( self.interactor.invoker().autorouter(), &self.appearance_panel, interactive_input.pointer_pos, ); } _ => {} } } } pub fn update_state( &mut self, tr: &Translator, error_dialog: &mut ErrorDialog, interactive_input: &InteractiveInput, ) -> ControlFlow<()> { if let Ok(data) = self.history_channel.1.try_recv() { match data { Ok(Ok(data)) => { self.interactor.replay(data); } Ok(Err(err)) => { error_dialog.push_error( "tr-module-history-file-loader", format!( "{}; {}", tr.text("tr-error-failed-to-parse-as-history-json"), err ), ); } Err(err) => { error_dialog.push_error( "tr-module-history-file-loader", format!("{}; {}", tr.text("tr-error-unable-to-read-file"), err), ); } } } match self.interactor.update(interactive_input) { ControlFlow::Continue(()) => ControlFlow::Continue(()), ControlFlow::Break(Ok(())) => ControlFlow::Break(()), ControlFlow::Break(Err(err)) => { error_dialog.push_error("tr-module-invoker", format!("{}", err)); ControlFlow::Break(()) } } } pub fn update_appearance_panel(&mut self, ctx: &egui::Context) { self.appearance_panel .update(ctx, self.interactor.invoker().autorouter().board()); } }