From 69f2ce3c2e51b85c704f187ff8a54e0786d62f7b Mon Sep 17 00:00:00 2001 From: Alain Emilia Anna Zscheile Date: Thu, 3 Oct 2024 15:07:02 +0200 Subject: [PATCH] egui: add structure Workspace for two-phase workflow --- src/bin/topola-egui/app.rs | 132 +++--------- src/bin/topola-egui/main.rs | 1 + src/bin/topola-egui/menu_bar.rs | 203 +++++++++---------- src/bin/topola-egui/status_bar.rs | 2 +- src/bin/topola-egui/viewport.rs | 327 ++++++++++++++---------------- src/bin/topola-egui/workspace.rs | 121 +++++++++++ 6 files changed, 399 insertions(+), 387 deletions(-) create mode 100644 src/bin/topola-egui/workspace.rs diff --git a/src/bin/topola-egui/app.rs b/src/bin/topola-egui/app.rs index 740c6e7..fe45ec2 100644 --- a/src/bin/topola-egui/app.rs +++ b/src/bin/topola-egui/app.rs @@ -29,36 +29,24 @@ use crate::{ status_bar::StatusBar, translator::Translator, viewport::Viewport, + workspace::Workspace, }; pub struct App { config: Config, translator: Translator, - maybe_overlay: Option, - - arc_mutex_maybe_invoker: Arc>>>, - - maybe_activity: Option, - content_channel: ( Sender>, Receiver>, ), - history_channel: ( - Sender>>, - Receiver>>, - ), viewport: Viewport, - menu_bar: MenuBar, status_bar: StatusBar, - error_dialog: ErrorDialog, - maybe_layers: Option, - maybe_design: Option, + maybe_workspace: Option, update_counter: f32, } @@ -68,17 +56,12 @@ impl Default for App { Self { config: Config::default(), translator: Translator::new(langid!("en-US")), - maybe_overlay: None, - arc_mutex_maybe_invoker: Arc::new(Mutex::new(None)), - maybe_activity: None, content_channel: channel(), - history_channel: channel(), viewport: Viewport::new(), menu_bar: MenuBar::new(), status_bar: StatusBar::new(), error_dialog: ErrorDialog::new(), - maybe_layers: None, - maybe_design: None, + maybe_workspace: None, update_counter: 0.0, } } @@ -113,8 +96,11 @@ impl App { fn update_state(&mut self) -> bool { if let Ok(data) = self.content_channel.1.try_recv() { match data { - Ok(design) => match self.load_specctra_dsn(design) { - Ok(()) => {} + Ok(design) => match Workspace::new(design, &self.translator) { + Ok(ws) => { + self.maybe_workspace = Some(ws); + self.viewport.scheduled_zoom_to_fit = true; + } Err(err) => { self.error_dialog .push_error("tr-module-specctra-dsn-file-loader", err); @@ -144,76 +130,11 @@ impl App { } } - if let Some(invoker) = self.arc_mutex_maybe_invoker.lock().unwrap().as_mut() { - if let Ok(data) = self.history_channel.1.try_recv() { - let tr = &self.translator; - match data { - Ok(Ok(data)) => { - invoker.replay(data); - } - Ok(Err(err)) => { - self.error_dialog.push_error( - "tr-module-history-file-loader", - format!( - "{}; {}", - tr.text("tr-error-failed-to-parse-as-history-json"), - err - ), - ); - } - Err(err) => { - self.error_dialog.push_error( - "tr-module-history-file-loader", - format!("{}; {}", tr.text("tr-error-unable-to-read-file"), err), - ); - } - } - } - - if let Some(ref mut activity) = self.maybe_activity { - return match activity.step(&mut ActivityContext { - interaction: InteractionContext {}, - invoker, - }) { - Ok(ActivityStatus::Running) => true, - Ok(ActivityStatus::Finished(..)) => false, - Err(err) => { - self.error_dialog - .push_error("tr-module-invoker", format!("{}", err)); - false - } - }; - } + if let Some(workspace) = &mut self.maybe_workspace { + return workspace.update_state(&self.translator, &mut self.error_dialog); } - false } - - fn load_specctra_dsn(&mut self, design: SpecctraDesign) -> Result<(), String> { - let tr = &self.translator; - let board = design.make_board(); - let overlay = Overlay::new(&board).map_err(|err| { - format!( - "{}; {}", - tr.text("tr-error-unable-to-initialize-overlay"), - err - ) - })?; - let layers = Layers::new(&board); - let autorouter = Autorouter::new(board).map_err(|err| { - format!( - "{}; {}", - tr.text("tr-error-unable-to-initialize-autorouter"), - err - ) - })?; - self.maybe_overlay = Some(overlay); - self.maybe_layers = Some(layers); - self.maybe_design = Some(design); - self.arc_mutex_maybe_invoker = Arc::new(Mutex::new(Some(Invoker::new(autorouter)))); - self.viewport.scheduled_zoom_to_fit = true; - Ok(()) - } } impl eframe::App for App { @@ -228,37 +149,32 @@ impl eframe::App for App { ctx, &self.translator, self.content_channel.0.clone(), - self.history_channel.0.clone(), - self.arc_mutex_maybe_invoker.clone(), - &mut self.maybe_activity, &mut self.viewport, - &mut self.maybe_overlay, - &self.maybe_design, + self.maybe_workspace.as_mut(), ); self.advance_state_by_dt(ctx.input(|i| i.stable_dt)); - self.status_bar - .update(ctx, &self.translator, &self.viewport, &self.maybe_activity); + self.status_bar.update( + ctx, + &self.translator, + &self.viewport, + self.maybe_workspace + .as_ref() + .and_then(|w| w.maybe_activity.as_ref()), + ); if self.menu_bar.show_layer_manager { - if let Some(ref mut layers) = self.maybe_layers { - if let Some(invoker) = self.arc_mutex_maybe_invoker.lock().unwrap().as_ref() { - layers.update(ctx, invoker.autorouter().board()); - } + if let Some(workspace) = &mut self.maybe_workspace { + workspace.update_layers(ctx); } } self.error_dialog.update(ctx, &self.translator); - let _viewport_rect = self.viewport.update( - ctx, - &self.menu_bar, - &mut self.arc_mutex_maybe_invoker.lock().unwrap(), - &mut self.maybe_activity, - &mut self.maybe_overlay, - &self.maybe_layers, - ); + let _viewport_rect = + self.viewport + .update(ctx, &self.menu_bar, self.maybe_workspace.as_mut()); if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { ctx.send_viewport_cmd(egui::ViewportCommand::Close); diff --git a/src/bin/topola-egui/main.rs b/src/bin/topola-egui/main.rs index 6eb4408..240d48e 100644 --- a/src/bin/topola-egui/main.rs +++ b/src/bin/topola-egui/main.rs @@ -13,6 +13,7 @@ mod painter; mod status_bar; mod translator; mod viewport; +mod workspace; use app::App; use sys_locale::get_locale; diff --git a/src/bin/topola-egui/menu_bar.rs b/src/bin/topola-egui/menu_bar.rs index cd7830e..9879504 100644 --- a/src/bin/topola-egui/menu_bar.rs +++ b/src/bin/topola-egui/menu_bar.rs @@ -26,6 +26,7 @@ use crate::{ overlay::Overlay, translator::Translator, viewport::Viewport, + workspace::Workspace, }; pub struct MenuBar { @@ -64,12 +65,8 @@ impl MenuBar { ctx: &egui::Context, tr: &Translator, content_sender: Sender>, - history_sender: Sender>>, - arc_mutex_maybe_invoker: Arc>>>, - maybe_activity: &mut Option, viewport: &mut Viewport, - maybe_overlay: &mut Option, - maybe_design: &Option, + maybe_workspace: Option<&mut Workspace>, ) -> Result<(), InvokerError> { let mut open_design = Trigger::new(Action::new( tr.text("tr-menu-file-open"), @@ -258,53 +255,57 @@ impl MenuBar { } }); } else if export_session.consume_key_triggered(ctx, ui) { - if let Some(design) = maybe_design { - if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_ref() { - let ctx = ui.ctx().clone(); - let board = invoker.autorouter().board(); + if let Some(workspace) = maybe_workspace { + let invoker = workspace.invoker.lock().unwrap(); + let ctx = ui.ctx().clone(); + let board = invoker.autorouter().board(); - // FIXME: I don't know how to avoid buffering the entire exported file - let mut writebuf = vec![]; + // FIXME: I don't know how to avoid buffering the entire exported file + let mut writebuf = vec![]; - design.write_ses(board, &mut writebuf); + workspace.design.write_ses(board, &mut writebuf); - let mut dialog = rfd::AsyncFileDialog::new(); - if let Some(filename) = Path::new(design.get_name()).file_stem() { - if let Some(filename) = filename.to_str() { - dialog = dialog.set_file_name(filename); - } + let mut dialog = rfd::AsyncFileDialog::new(); + if let Some(filename) = Path::new(workspace.design.get_name()).file_stem() { + if let Some(filename) = filename.to_str() { + dialog = dialog.set_file_name(filename); } - let task = dialog - .add_filter(tr.text("tr-menu-open-specctra-session-file"), &["ses"]) - .save_file(); - - execute(async move { - if let Some(file_handle) = task.await { - file_handle.write(&writebuf).await; - ctx.request_repaint(); - } - }); } + + let task = dialog + .add_filter(tr.text("tr-menu-open-specctra-session-file"), &["ses"]) + .save_file(); + + execute(async move { + if let Some(file_handle) = task.await { + file_handle.write(&writebuf).await; + ctx.request_repaint(); + } + }); } } else if import_history.consume_key_triggered(ctx, ui) { - let ctx = ctx.clone(); - let task = rfd::AsyncFileDialog::new().pick_file(); + if let Some(workspace) = maybe_workspace { + let ctx = ctx.clone(); + let task = rfd::AsyncFileDialog::new().pick_file(); + let history_sender = workspace.history_channel.0.clone(); - execute(async move { - if let Some(file_handle) = task.await { - let data = handle_file(&file_handle).await.and_then(|data| { - match serde_json::from_reader(data) { - Ok(history) => Ok(Ok(history)), - Err(err) if err.is_io() => Err(err.into()), - Err(err) => Ok(Err(err)), - } - }); - history_sender.send(data); - ctx.request_repaint(); - } - }); + execute(async move { + if let Some(file_handle) = task.await { + let data = handle_file(&file_handle).await.and_then(|data| { + match serde_json::from_reader(data) { + Ok(history) => Ok(Ok(history)), + Err(err) if err.is_io() => Err(err.into()), + Err(err) => Ok(Err(err)), + } + }); + history_sender.send(data); + ctx.request_repaint(); + } + }); + } } else if export_history.consume_key_triggered(ctx, ui) { - if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_ref() { + if let Some(workspace) = maybe_workspace { + let invoker = workspace.invoker.lock().unwrap(); let ctx = ctx.clone(); let task = rfd::AsyncFileDialog::new().save_file(); @@ -322,87 +323,81 @@ impl MenuBar { } else if quit.consume_key_triggered(ctx, ui) { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } else if undo.consume_key_triggered(ctx, ui) { - if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { - invoker.undo(); + if let Some(workspace) = maybe_workspace { + workspace.invoker.lock().unwrap().undo(); } } else if redo.consume_key_triggered(ctx, ui) { - if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { - invoker.redo(); + if let Some(workspace) = maybe_workspace { + workspace.invoker.lock().unwrap().redo(); } } else if abort.consume_key_triggered(ctx, ui) { - if let Some(activity) = maybe_activity { - if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { + if let Some(workspace) = maybe_workspace { + if let Some(activity) = &mut workspace.maybe_activity { activity.abort(&mut ActivityContext { interaction: InteractionContext {}, - invoker, + invoker: &mut *workspace.invoker.lock().unwrap(), }); } } } else if remove_bands.consume_key_triggered(ctx, ui) { - if maybe_activity.as_mut().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) - }) { - if let (Some(invoker), Some(ref mut overlay)) = ( - arc_mutex_maybe_invoker.lock().unwrap().as_mut(), - maybe_overlay, - ) { - let selection = overlay.take_selection(); - *maybe_activity = Some(ActivityStepperWithStatus::new_execution( - invoker.execute_stepper(Command::RemoveBands( - selection.band_selection, - ))?, - )); + if let Some(workspace) = maybe_workspace { + if workspace.maybe_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) + }) { + let mut invoker = workspace.invoker.lock().unwrap(); + let selection = workspace.overlay.take_selection(); + workspace.maybe_activity = Some( + ActivityStepperWithStatus::new_execution(invoker.execute_stepper( + Command::RemoveBands(selection.band_selection), + )?), + ); } } } else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) { } else if autoroute.consume_key_triggered(ctx, ui) { - if maybe_activity.as_mut().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) - }) { - if let (Some(invoker), Some(ref mut overlay)) = ( - arc_mutex_maybe_invoker.lock().unwrap().as_mut(), - maybe_overlay, - ) { - let selection = overlay.take_selection(); - *maybe_activity = Some(ActivityStepperWithStatus::new_execution( - invoker.execute_stepper(Command::Autoroute( - selection.pin_selection, - self.autorouter_options, - ))?, - )); + if let Some(workspace) = maybe_workspace { + if workspace.maybe_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) + }) { + let mut invoker = workspace.invoker.lock().unwrap(); + let selection = workspace.overlay.take_selection(); + workspace.maybe_activity = + Some(ActivityStepperWithStatus::new_execution( + invoker.execute_stepper(Command::Autoroute( + selection.pin_selection, + self.autorouter_options, + ))?, + )); } } } else if compare_detours.consume_key_triggered(ctx, ui) { - if maybe_activity.as_mut().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) - }) { - if let (Some(invoker), Some(ref mut overlay)) = ( - arc_mutex_maybe_invoker.lock().unwrap().as_mut(), - maybe_overlay, - ) { - let selection = overlay.take_selection(); - *maybe_activity = Some(ActivityStepperWithStatus::new_execution( - invoker.execute_stepper(Command::CompareDetours( - selection.pin_selection, - self.autorouter_options, - ))?, - )); + if let Some(workspace) = maybe_workspace { + if workspace.maybe_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) + }) { + let mut invoker = workspace.invoker.lock().unwrap(); + let selection = workspace.overlay.take_selection(); + workspace.maybe_activity = + Some(ActivityStepperWithStatus::new_execution( + invoker.execute_stepper(Command::CompareDetours( + selection.pin_selection, + self.autorouter_options, + ))?, + )); } } } else if measure_length.consume_key_triggered(ctx, ui) { - if maybe_activity.as_mut().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) - }) { - if let (Some(invoker), Some(ref mut overlay)) = ( - arc_mutex_maybe_invoker.lock().unwrap().as_mut(), - maybe_overlay, - ) { - let selection = overlay.take_selection(); - *maybe_activity = Some(ActivityStepperWithStatus::new_execution( - invoker.execute_stepper(Command::MeasureLength( - selection.band_selection, - ))?, - )); + if let Some(workspace) = maybe_workspace { + if workspace.maybe_activity.as_mut().map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) + }) { + let mut invoker = workspace.invoker.lock().unwrap(); + let selection = workspace.overlay.take_selection(); + workspace.maybe_activity = Some( + ActivityStepperWithStatus::new_execution(invoker.execute_stepper( + Command::MeasureLength(selection.band_selection), + )?), + ); } } } diff --git a/src/bin/topola-egui/status_bar.rs b/src/bin/topola-egui/status_bar.rs index fd9a1ea..6ac9692 100644 --- a/src/bin/topola-egui/status_bar.rs +++ b/src/bin/topola-egui/status_bar.rs @@ -16,7 +16,7 @@ impl StatusBar { ctx: &egui::Context, tr: &Translator, viewport: &Viewport, - maybe_activity: &Option, + maybe_activity: Option<&ActivityStepperWithStatus>, ) { egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { let latest_pos = viewport.transform.inverse() diff --git a/src/bin/topola-egui/viewport.rs b/src/bin/topola-egui/viewport.rs index 18de39f..9f39c52 100644 --- a/src/bin/topola-egui/viewport.rs +++ b/src/bin/topola-egui/viewport.rs @@ -21,7 +21,7 @@ use topola::{ use crate::{ activity::ActivityStepperWithStatus, layers::Layers, menu_bar::MenuBar, overlay::Overlay, - painter::Painter, + painter::Painter, workspace::Workspace, }; pub struct Viewport { @@ -41,22 +41,13 @@ impl Viewport { &mut self, ctx: &egui::Context, top: &MenuBar, - maybe_invoker: &mut Option>, - maybe_activity: &mut Option, - maybe_overlay: &mut Option, - maybe_layers: &Option, + mut maybe_workspace: Option<&mut Workspace>, ) -> egui::Rect { - let viewport_rect = self.paint( - ctx, - top, - maybe_invoker, - maybe_activity, - maybe_overlay, - maybe_layers, - ); + let viewport_rect = self.paint(ctx, top, maybe_workspace.as_deref_mut()); if self.scheduled_zoom_to_fit { - self.zoom_to_fit(maybe_invoker, &viewport_rect); + let mut maybe_invoker = maybe_workspace.as_ref().map(|w| w.invoker.lock().unwrap()); + self.zoom_to_fit(maybe_invoker.as_deref_mut(), &viewport_rect); } viewport_rect @@ -66,10 +57,7 @@ impl Viewport { &mut self, ctx: &egui::Context, top: &MenuBar, - maybe_invoker: &mut Option>, - maybe_activity: &mut Option, - maybe_overlay: &mut Option, - maybe_layers: &Option, + maybe_workspace: Option<&mut Workspace>, ) -> egui::Rect { egui::CentralPanel::default().show(ctx, |ui| { egui::Frame::canvas(ui.style()).show(ui, |ui| { @@ -86,7 +74,11 @@ impl Viewport { let mut painter = Painter::new(ui, self.transform, top.show_bboxes); - if let Some(ref mut invoker) = maybe_invoker { + if let Some(workspace) = maybe_workspace { + let mut invoker = workspace.invoker.lock().unwrap(); + let layers = &mut workspace.layers; + let overlay = &mut workspace.overlay; + if ctx.input(|i| i.pointer.any_click()) { if top.is_placing_via { invoker.execute( @@ -100,7 +92,7 @@ impl Viewport { maybe_net: Some(1234), }), ); - } else if let Some(overlay) = maybe_overlay { + } else { overlay.click( invoker.autorouter().board(), point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, @@ -108,169 +100,156 @@ impl Viewport { } } - if let (Some(invoker), Some(overlay)) = ( - maybe_invoker, - maybe_overlay, - ) { - let board = invoker.autorouter().board(); + let board = invoker.autorouter().board(); + for i in (0..layers.visible.len()).rev() { + if layers.visible[i] { + for primitive in board.layout().drawing().layer_primitive_nodes(i) { + let shape = primitive.primitive(board.layout().drawing()).shape(); - if let Some(layers) = maybe_layers { - for i in (0..layers.visible.len()).rev() { - if layers.visible[i] { - for primitive in board.layout().drawing().layer_primitive_nodes(i) { - let shape = primitive.primitive(board.layout().drawing()).shape(); - - let color = if overlay - .selection() - .contains_node(board, GenericNode::Primitive(primitive)) - { - layers.highlight_colors[i] - } else { - if let Some(activity) = maybe_activity { - if activity.obstacles().contains(&primitive) { - layers.highlight_colors[i] - } else { - layers.colors[i] - } - } else { - layers.colors[i] - } - }; - - painter.paint_primitive(&shape, color); + let color = if overlay + .selection() + .contains_node(board, GenericNode::Primitive(primitive)) + { + layers.highlight_colors[i] + } else if let Some(activity) = &mut workspace.maybe_activity { + if activity.obstacles().contains(&primitive) { + layers.highlight_colors[i] + } else { + layers.colors[i] } + } else { + layers.colors[i] + }; - for poly in board.layout().layer_poly_nodes(i) { - let color = if overlay - .selection() - .contains_node(board, GenericNode::Compound(poly.into())) - { - layers.highlight_colors[i] - } else { - layers.colors[i] - }; - - painter.paint_polygon(&board.layout().poly(poly).shape().polygon, color) - } - } - } - } - - if top.show_ratsnest { - for edge in overlay.ratsnest().graph().edge_references() { - let from = overlay - .ratsnest() - .graph() - .node_weight(edge.source()) - .unwrap() - .pos; - let to = overlay - .ratsnest() - .graph() - .node_weight(edge.target()) - .unwrap() - .pos; - - painter.paint_edge( - from, - to, - egui::Stroke::new(1.0, egui::Color32::from_rgb(90, 90, 200)), - ); - } - } - - if top.show_navmesh { - if let Some(activity) = maybe_activity { - if let Some(navmesh) = activity.maybe_navmesh() { - for edge in navmesh.edge_references() { - let mut from = PrimitiveIndex::from(navmesh.node_weight(edge.source()).unwrap().node) - .primitive(board.layout().drawing()) - .shape() - .center(); - let mut to = PrimitiveIndex::from(navmesh.node_weight(edge.target()).unwrap().node) - .primitive(board.layout().drawing()) - .shape() - .center(); - - if let Some(from_cw) = navmesh.node_weight(edge.source()).unwrap().maybe_cw { - if from_cw { - from -= [0.0, 150.0].into(); - } else { - from += [0.0, 150.0].into(); - } - } - - if let Some(to_cw) = navmesh.node_weight(edge.target()).unwrap().maybe_cw { - if to_cw { - to -= [0.0, 150.0].into(); - } else { - to += [0.0, 150.0].into(); - } - } - - let stroke = 'blk: { - if let (Some(source_pos), Some(target_pos)) = ( - activity.maybe_trace().map(|trace| - trace.path - .iter() - .position(|node| *node == edge.source())).flatten(), - activity.maybe_trace().map(|trace| - trace.path - .iter() - .position(|node| *node == edge.target())).flatten(), - ) { - if target_pos == source_pos + 1 - || source_pos == target_pos + 1 - { - break 'blk egui::Stroke::new( - 5.0, - egui::Color32::from_rgb(250, 250, 0), - ); - } - } - - egui::Stroke::new(1.0, egui::Color32::from_rgb(125, 125, 125)) - }; - - painter.paint_edge(from, to, stroke); - } - } - } - } - - if top.show_bboxes { - let root_bbox3d = board.layout().drawing().rtree().root().envelope(); - - let root_bbox = AABB::<[f64; 2]>::from_corners([root_bbox3d.lower()[0], root_bbox3d.lower()[1]].into(), [root_bbox3d.upper()[0], root_bbox3d.upper()[1]].into()); - painter.paint_bbox(root_bbox); - } - - if let Some(activity) = maybe_activity { - for ghost in activity.ghosts().iter() { - painter.paint_primitive(&ghost, egui::Color32::from_rgb(75, 75, 150)); + painter.paint_primitive(&shape, color); } + for poly in board.layout().layer_poly_nodes(i) { + let color = if overlay + .selection() + .contains_node(board, GenericNode::Compound(poly.into())) + { + layers.highlight_colors[i] + } else { + layers.colors[i] + }; + + painter.paint_polygon(&board.layout().poly(poly).shape().polygon, color) + } + } + } + + if top.show_ratsnest { + let graph = overlay.ratsnest().graph(); + for edge in graph.edge_references() { + let from = graph + .node_weight(edge.source()) + .unwrap() + .pos; + let to = graph + .node_weight(edge.target()) + .unwrap() + .pos; + + painter.paint_edge( + from, + to, + egui::Stroke::new(1.0, egui::Color32::from_rgb(90, 90, 200)), + ); + } + } + + if top.show_navmesh { + if let Some(activity) = &mut workspace.maybe_activity { if let Some(navmesh) = activity.maybe_navmesh() { - if top.show_origin_destination { - let (origin, destination) = (navmesh.origin(), navmesh.destination()); - painter.paint_dot( - Circle { - pos: board.layout().drawing().primitive(origin).shape().center(), - r: 150.0, - }, - egui::Color32::from_rgb(255, 255, 100), - ); - painter.paint_dot( - Circle { - pos: board.layout().drawing().primitive(destination).shape().center(), - r: 150.0, - }, - egui::Color32::from_rgb(255, 255, 100), - ); + for edge in navmesh.edge_references() { + let mut from = PrimitiveIndex::from(navmesh.node_weight(edge.source()).unwrap().node) + .primitive(board.layout().drawing()) + .shape() + .center(); + let mut to = PrimitiveIndex::from(navmesh.node_weight(edge.target()).unwrap().node) + .primitive(board.layout().drawing()) + .shape() + .center(); + + if let Some(from_cw) = navmesh.node_weight(edge.source()).unwrap().maybe_cw { + if from_cw { + from -= [0.0, 150.0].into(); + } else { + from += [0.0, 150.0].into(); + } + } + + if let Some(to_cw) = navmesh.node_weight(edge.target()).unwrap().maybe_cw { + if to_cw { + to -= [0.0, 150.0].into(); + } else { + to += [0.0, 150.0].into(); + } + } + + let stroke = 'blk: { + if let (Some(source_pos), Some(target_pos)) = ( + activity.maybe_trace().map(|trace| + trace.path + .iter() + .position(|node| *node == edge.source())).flatten(), + activity.maybe_trace().map(|trace| + trace.path + .iter() + .position(|node| *node == edge.target())).flatten(), + ) { + if target_pos == source_pos + 1 + || source_pos == target_pos + 1 + { + break 'blk egui::Stroke::new( + 5.0, + egui::Color32::from_rgb(250, 250, 0), + ); + } + } + + egui::Stroke::new(1.0, egui::Color32::from_rgb(125, 125, 125)) + }; + + painter.paint_edge(from, to, stroke); } } } } + + if top.show_bboxes { + let root_bbox3d = board.layout().drawing().rtree().root().envelope(); + + let root_bbox = AABB::<[f64; 2]>::from_corners([root_bbox3d.lower()[0], root_bbox3d.lower()[1]].into(), [root_bbox3d.upper()[0], root_bbox3d.upper()[1]].into()); + painter.paint_bbox(root_bbox); + } + + if let Some(activity) = &mut workspace.maybe_activity { + for ghost in activity.ghosts().iter() { + painter.paint_primitive(&ghost, egui::Color32::from_rgb(75, 75, 150)); + } + + if let Some(navmesh) = activity.maybe_navmesh() { + if top.show_origin_destination { + let (origin, destination) = (navmesh.origin(), navmesh.destination()); + painter.paint_dot( + Circle { + pos: board.layout().drawing().primitive(origin).shape().center(), + r: 150.0, + }, + egui::Color32::from_rgb(255, 255, 100), + ); + painter.paint_dot( + Circle { + pos: board.layout().drawing().primitive(destination).shape().center(), + r: 150.0, + }, + egui::Color32::from_rgb(255, 255, 100), + ); + } + } + } } viewport_rect @@ -280,7 +259,7 @@ impl Viewport { fn zoom_to_fit( &mut self, - maybe_invoker: &mut Option>, + maybe_invoker: Option<&mut Invoker>, viewport_rect: &egui::Rect, ) { if self.scheduled_zoom_to_fit { diff --git a/src/bin/topola-egui/workspace.rs b/src/bin/topola-egui/workspace.rs new file mode 100644 index 0000000..d8f7b70 --- /dev/null +++ b/src/bin/topola-egui/workspace.rs @@ -0,0 +1,121 @@ +use serde::{Deserialize, Serialize}; +use std::{ + future::Future, + io, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, +}; +use unic_langid::{langid, LanguageIdentifier}; + +use topola::{ + autorouter::{history::History, invoker::Invoker, Autorouter}, + specctra::{ + design::{LoadingError as SpecctraLoadingError, SpecctraDesign}, + mesadata::SpecctraMesadata, + }, + stepper::Step, +}; + +use crate::{ + activity::{ActivityContext, ActivityStatus, ActivityStepperWithStatus}, + error_dialog::ErrorDialog, + interaction::InteractionContext, + layers::Layers, + menu_bar::MenuBar, + overlay::Overlay, + status_bar::StatusBar, + translator::Translator, + viewport::Viewport, +}; + +/// A loaded design and associated structures +pub struct Workspace { + pub design: SpecctraDesign, + pub layers: Layers, + pub overlay: Overlay, + pub invoker: Arc>>, + + pub maybe_activity: Option, + + pub history_channel: ( + Sender>>, + Receiver>>, + ), +} + +impl Workspace { + pub fn new(design: SpecctraDesign, tr: &Translator) -> Result { + let board = design.make_board(); + let overlay = Overlay::new(&board).map_err(|err| { + format!( + "{}; {}", + tr.text("tr-error_unable-to-initialize-overlay"), + err + ) + })?; + let layers = Layers::new(&board); + let autorouter = Autorouter::new(board).map_err(|err| { + format!( + "{}; {}", + tr.text("tr-error_unable-to-initialize-autorouter"), + err + ) + })?; + Ok(Self { + design, + layers, + overlay, + invoker: Arc::new(Mutex::new(Invoker::new(autorouter))), + maybe_activity: None, + history_channel: channel(), + }) + } + + pub fn update_state(&mut self, tr: &Translator, error_dialog: &mut ErrorDialog) -> bool { + if let Ok(data) = self.history_channel.1.try_recv() { + match data { + Ok(Ok(data)) => { + self.invoker.lock().unwrap().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), + ); + } + } + } + + if let Some(activity) = &mut self.maybe_activity { + return match activity.step(&mut ActivityContext { + interaction: InteractionContext {}, + invoker: &mut *self.invoker.lock().unwrap(), + }) { + Ok(ActivityStatus::Running) => true, + Ok(ActivityStatus::Finished(..)) => false, + Err(err) => { + error_dialog.push_error("tr-module-invoker", format!("{}", err)); + false + } + }; + } + false + } + + pub fn update_layers(&mut self, ctx: &egui::Context) { + let invoker = self.invoker.lock().unwrap(); + self.layers.update(ctx, invoker.autorouter().board()); + } +}