egui: add structure Workspace for two-phase workflow

This commit is contained in:
Alain Emilia Anna Zscheile 2024-10-03 15:07:02 +02:00
parent f02b7be878
commit 69f2ce3c2e
6 changed files with 399 additions and 387 deletions

View File

@ -29,36 +29,24 @@ use crate::{
status_bar::StatusBar, status_bar::StatusBar,
translator::Translator, translator::Translator,
viewport::Viewport, viewport::Viewport,
workspace::Workspace,
}; };
pub struct App { pub struct App {
config: Config, config: Config,
translator: Translator, translator: Translator,
maybe_overlay: Option<Overlay>,
arc_mutex_maybe_invoker: Arc<Mutex<Option<Invoker<SpecctraMesadata>>>>,
maybe_activity: Option<ActivityStepperWithStatus>,
content_channel: ( content_channel: (
Sender<Result<SpecctraDesign, SpecctraLoadingError>>, Sender<Result<SpecctraDesign, SpecctraLoadingError>>,
Receiver<Result<SpecctraDesign, SpecctraLoadingError>>, Receiver<Result<SpecctraDesign, SpecctraLoadingError>>,
), ),
history_channel: (
Sender<std::io::Result<Result<History, serde_json::Error>>>,
Receiver<std::io::Result<Result<History, serde_json::Error>>>,
),
viewport: Viewport, viewport: Viewport,
menu_bar: MenuBar, menu_bar: MenuBar,
status_bar: StatusBar, status_bar: StatusBar,
error_dialog: ErrorDialog, error_dialog: ErrorDialog,
maybe_layers: Option<Layers>, maybe_workspace: Option<Workspace>,
maybe_design: Option<SpecctraDesign>,
update_counter: f32, update_counter: f32,
} }
@ -68,17 +56,12 @@ impl Default for App {
Self { Self {
config: Config::default(), config: Config::default(),
translator: Translator::new(langid!("en-US")), translator: Translator::new(langid!("en-US")),
maybe_overlay: None,
arc_mutex_maybe_invoker: Arc::new(Mutex::new(None)),
maybe_activity: None,
content_channel: channel(), content_channel: channel(),
history_channel: channel(),
viewport: Viewport::new(), viewport: Viewport::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
status_bar: StatusBar::new(), status_bar: StatusBar::new(),
error_dialog: ErrorDialog::new(), error_dialog: ErrorDialog::new(),
maybe_layers: None, maybe_workspace: None,
maybe_design: None,
update_counter: 0.0, update_counter: 0.0,
} }
} }
@ -113,8 +96,11 @@ impl App {
fn update_state(&mut self) -> bool { fn update_state(&mut self) -> bool {
if let Ok(data) = self.content_channel.1.try_recv() { if let Ok(data) = self.content_channel.1.try_recv() {
match data { match data {
Ok(design) => match self.load_specctra_dsn(design) { Ok(design) => match Workspace::new(design, &self.translator) {
Ok(()) => {} Ok(ws) => {
self.maybe_workspace = Some(ws);
self.viewport.scheduled_zoom_to_fit = true;
}
Err(err) => { Err(err) => {
self.error_dialog self.error_dialog
.push_error("tr-module-specctra-dsn-file-loader", err); .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 Some(workspace) = &mut self.maybe_workspace {
if let Ok(data) = self.history_channel.1.try_recv() { return workspace.update_state(&self.translator, &mut self.error_dialog);
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
}
};
}
} }
false 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 { impl eframe::App for App {
@ -228,37 +149,32 @@ impl eframe::App for App {
ctx, ctx,
&self.translator, &self.translator,
self.content_channel.0.clone(), 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.viewport,
&mut self.maybe_overlay, self.maybe_workspace.as_mut(),
&self.maybe_design,
); );
self.advance_state_by_dt(ctx.input(|i| i.stable_dt)); self.advance_state_by_dt(ctx.input(|i| i.stable_dt));
self.status_bar self.status_bar.update(
.update(ctx, &self.translator, &self.viewport, &self.maybe_activity); 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 self.menu_bar.show_layer_manager {
if let Some(ref mut layers) = self.maybe_layers { if let Some(workspace) = &mut self.maybe_workspace {
if let Some(invoker) = self.arc_mutex_maybe_invoker.lock().unwrap().as_ref() { workspace.update_layers(ctx);
layers.update(ctx, invoker.autorouter().board());
}
} }
} }
self.error_dialog.update(ctx, &self.translator); self.error_dialog.update(ctx, &self.translator);
let _viewport_rect = self.viewport.update( let _viewport_rect =
ctx, self.viewport
&self.menu_bar, .update(ctx, &self.menu_bar, self.maybe_workspace.as_mut());
&mut self.arc_mutex_maybe_invoker.lock().unwrap(),
&mut self.maybe_activity,
&mut self.maybe_overlay,
&self.maybe_layers,
);
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);

View File

@ -13,6 +13,7 @@ mod painter;
mod status_bar; mod status_bar;
mod translator; mod translator;
mod viewport; mod viewport;
mod workspace;
use app::App; use app::App;
use sys_locale::get_locale; use sys_locale::get_locale;

View File

@ -26,6 +26,7 @@ use crate::{
overlay::Overlay, overlay::Overlay,
translator::Translator, translator::Translator,
viewport::Viewport, viewport::Viewport,
workspace::Workspace,
}; };
pub struct MenuBar { pub struct MenuBar {
@ -64,12 +65,8 @@ impl MenuBar {
ctx: &egui::Context, ctx: &egui::Context,
tr: &Translator, tr: &Translator,
content_sender: Sender<Result<SpecctraDesign, SpecctraLoadingError>>, content_sender: Sender<Result<SpecctraDesign, SpecctraLoadingError>>,
history_sender: Sender<std::io::Result<Result<History, serde_json::Error>>>,
arc_mutex_maybe_invoker: Arc<Mutex<Option<Invoker<SpecctraMesadata>>>>,
maybe_activity: &mut Option<ActivityStepperWithStatus>,
viewport: &mut Viewport, viewport: &mut Viewport,
maybe_overlay: &mut Option<Overlay>, maybe_workspace: Option<&mut Workspace>,
maybe_design: &Option<SpecctraDesign>,
) -> Result<(), InvokerError> { ) -> Result<(), InvokerError> {
let mut open_design = Trigger::new(Action::new( let mut open_design = Trigger::new(Action::new(
tr.text("tr-menu-file-open"), tr.text("tr-menu-file-open"),
@ -258,53 +255,57 @@ impl MenuBar {
} }
}); });
} else if export_session.consume_key_triggered(ctx, ui) { } else if export_session.consume_key_triggered(ctx, ui) {
if let Some(design) = maybe_design { if let Some(workspace) = maybe_workspace {
if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_ref() { let invoker = workspace.invoker.lock().unwrap();
let ctx = ui.ctx().clone(); let ctx = ui.ctx().clone();
let board = invoker.autorouter().board(); let board = invoker.autorouter().board();
// FIXME: I don't know how to avoid buffering the entire exported file // FIXME: I don't know how to avoid buffering the entire exported file
let mut writebuf = vec![]; let mut writebuf = vec![];
design.write_ses(board, &mut writebuf); workspace.design.write_ses(board, &mut writebuf);
let mut dialog = rfd::AsyncFileDialog::new(); let mut dialog = rfd::AsyncFileDialog::new();
if let Some(filename) = Path::new(design.get_name()).file_stem() { if let Some(filename) = Path::new(workspace.design.get_name()).file_stem() {
if let Some(filename) = filename.to_str() { if let Some(filename) = filename.to_str() {
dialog = dialog.set_file_name(filename); 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) { } else if import_history.consume_key_triggered(ctx, ui) {
let ctx = ctx.clone(); if let Some(workspace) = maybe_workspace {
let task = rfd::AsyncFileDialog::new().pick_file(); let ctx = ctx.clone();
let task = rfd::AsyncFileDialog::new().pick_file();
let history_sender = workspace.history_channel.0.clone();
execute(async move { execute(async move {
if let Some(file_handle) = task.await { if let Some(file_handle) = task.await {
let data = handle_file(&file_handle).await.and_then(|data| { let data = handle_file(&file_handle).await.and_then(|data| {
match serde_json::from_reader(data) { match serde_json::from_reader(data) {
Ok(history) => Ok(Ok(history)), Ok(history) => Ok(Ok(history)),
Err(err) if err.is_io() => Err(err.into()), Err(err) if err.is_io() => Err(err.into()),
Err(err) => Ok(Err(err)), Err(err) => Ok(Err(err)),
} }
}); });
history_sender.send(data); history_sender.send(data);
ctx.request_repaint(); ctx.request_repaint();
} }
}); });
}
} else if export_history.consume_key_triggered(ctx, ui) { } 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 ctx = ctx.clone();
let task = rfd::AsyncFileDialog::new().save_file(); let task = rfd::AsyncFileDialog::new().save_file();
@ -322,87 +323,81 @@ impl MenuBar {
} else if quit.consume_key_triggered(ctx, ui) { } else if quit.consume_key_triggered(ctx, ui) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} else if undo.consume_key_triggered(ctx, ui) { } else if undo.consume_key_triggered(ctx, ui) {
if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { if let Some(workspace) = maybe_workspace {
invoker.undo(); workspace.invoker.lock().unwrap().undo();
} }
} else if redo.consume_key_triggered(ctx, ui) { } else if redo.consume_key_triggered(ctx, ui) {
if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { if let Some(workspace) = maybe_workspace {
invoker.redo(); workspace.invoker.lock().unwrap().redo();
} }
} else if abort.consume_key_triggered(ctx, ui) { } else if abort.consume_key_triggered(ctx, ui) {
if let Some(activity) = maybe_activity { if let Some(workspace) = maybe_workspace {
if let Some(invoker) = arc_mutex_maybe_invoker.lock().unwrap().as_mut() { if let Some(activity) = &mut workspace.maybe_activity {
activity.abort(&mut ActivityContext { activity.abort(&mut ActivityContext {
interaction: InteractionContext {}, interaction: InteractionContext {},
invoker, invoker: &mut *workspace.invoker.lock().unwrap(),
}); });
} }
} }
} else if remove_bands.consume_key_triggered(ctx, ui) { } else if remove_bands.consume_key_triggered(ctx, ui) {
if maybe_activity.as_mut().map_or(true, |activity| { if let Some(workspace) = maybe_workspace {
matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) if workspace.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(), let mut invoker = workspace.invoker.lock().unwrap();
maybe_overlay, let selection = workspace.overlay.take_selection();
) { workspace.maybe_activity = Some(
let selection = overlay.take_selection(); ActivityStepperWithStatus::new_execution(invoker.execute_stepper(
*maybe_activity = Some(ActivityStepperWithStatus::new_execution( Command::RemoveBands(selection.band_selection),
invoker.execute_stepper(Command::RemoveBands( )?),
selection.band_selection, );
))?,
));
} }
} }
} else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) { } else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) {
} else if autoroute.consume_key_triggered(ctx, ui) { } else if autoroute.consume_key_triggered(ctx, ui) {
if maybe_activity.as_mut().map_or(true, |activity| { if let Some(workspace) = maybe_workspace {
matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) if workspace.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(), let mut invoker = workspace.invoker.lock().unwrap();
maybe_overlay, let selection = workspace.overlay.take_selection();
) { workspace.maybe_activity =
let selection = overlay.take_selection(); Some(ActivityStepperWithStatus::new_execution(
*maybe_activity = Some(ActivityStepperWithStatus::new_execution( invoker.execute_stepper(Command::Autoroute(
invoker.execute_stepper(Command::Autoroute( selection.pin_selection,
selection.pin_selection, self.autorouter_options,
self.autorouter_options, ))?,
))?, ));
));
} }
} }
} else if compare_detours.consume_key_triggered(ctx, ui) { } else if compare_detours.consume_key_triggered(ctx, ui) {
if maybe_activity.as_mut().map_or(true, |activity| { if let Some(workspace) = maybe_workspace {
matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) if workspace.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(), let mut invoker = workspace.invoker.lock().unwrap();
maybe_overlay, let selection = workspace.overlay.take_selection();
) { workspace.maybe_activity =
let selection = overlay.take_selection(); Some(ActivityStepperWithStatus::new_execution(
*maybe_activity = Some(ActivityStepperWithStatus::new_execution( invoker.execute_stepper(Command::CompareDetours(
invoker.execute_stepper(Command::CompareDetours( selection.pin_selection,
selection.pin_selection, self.autorouter_options,
self.autorouter_options, ))?,
))?, ));
));
} }
} }
} else if measure_length.consume_key_triggered(ctx, ui) { } else if measure_length.consume_key_triggered(ctx, ui) {
if maybe_activity.as_mut().map_or(true, |activity| { if let Some(workspace) = maybe_workspace {
matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) if workspace.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(), let mut invoker = workspace.invoker.lock().unwrap();
maybe_overlay, let selection = workspace.overlay.take_selection();
) { workspace.maybe_activity = Some(
let selection = overlay.take_selection(); ActivityStepperWithStatus::new_execution(invoker.execute_stepper(
*maybe_activity = Some(ActivityStepperWithStatus::new_execution( Command::MeasureLength(selection.band_selection),
invoker.execute_stepper(Command::MeasureLength( )?),
selection.band_selection, );
))?,
));
} }
} }
} }

View File

@ -16,7 +16,7 @@ impl StatusBar {
ctx: &egui::Context, ctx: &egui::Context,
tr: &Translator, tr: &Translator,
viewport: &Viewport, viewport: &Viewport,
maybe_activity: &Option<ActivityStepperWithStatus>, maybe_activity: Option<&ActivityStepperWithStatus>,
) { ) {
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
let latest_pos = viewport.transform.inverse() let latest_pos = viewport.transform.inverse()

View File

@ -21,7 +21,7 @@ use topola::{
use crate::{ use crate::{
activity::ActivityStepperWithStatus, layers::Layers, menu_bar::MenuBar, overlay::Overlay, activity::ActivityStepperWithStatus, layers::Layers, menu_bar::MenuBar, overlay::Overlay,
painter::Painter, painter::Painter, workspace::Workspace,
}; };
pub struct Viewport { pub struct Viewport {
@ -41,22 +41,13 @@ impl Viewport {
&mut self, &mut self,
ctx: &egui::Context, ctx: &egui::Context,
top: &MenuBar, top: &MenuBar,
maybe_invoker: &mut Option<Invoker<SpecctraMesadata>>, mut maybe_workspace: Option<&mut Workspace>,
maybe_activity: &mut Option<ActivityStepperWithStatus>,
maybe_overlay: &mut Option<Overlay>,
maybe_layers: &Option<Layers>,
) -> egui::Rect { ) -> egui::Rect {
let viewport_rect = self.paint( let viewport_rect = self.paint(ctx, top, maybe_workspace.as_deref_mut());
ctx,
top,
maybe_invoker,
maybe_activity,
maybe_overlay,
maybe_layers,
);
if self.scheduled_zoom_to_fit { 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 viewport_rect
@ -66,10 +57,7 @@ impl Viewport {
&mut self, &mut self,
ctx: &egui::Context, ctx: &egui::Context,
top: &MenuBar, top: &MenuBar,
maybe_invoker: &mut Option<Invoker<SpecctraMesadata>>, maybe_workspace: Option<&mut Workspace>,
maybe_activity: &mut Option<ActivityStepperWithStatus>,
maybe_overlay: &mut Option<Overlay>,
maybe_layers: &Option<Layers>,
) -> egui::Rect { ) -> egui::Rect {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::Frame::canvas(ui.style()).show(ui, |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); 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 ctx.input(|i| i.pointer.any_click()) {
if top.is_placing_via { if top.is_placing_via {
invoker.execute( invoker.execute(
@ -100,7 +92,7 @@ impl Viewport {
maybe_net: Some(1234), maybe_net: Some(1234),
}), }),
); );
} else if let Some(overlay) = maybe_overlay { } else {
overlay.click( overlay.click(
invoker.autorouter().board(), invoker.autorouter().board(),
point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, 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)) = ( let board = invoker.autorouter().board();
maybe_invoker, for i in (0..layers.visible.len()).rev() {
maybe_overlay, if layers.visible[i] {
) { for primitive in board.layout().drawing().layer_primitive_nodes(i) {
let board = invoker.autorouter().board(); let shape = primitive.primitive(board.layout().drawing()).shape();
if let Some(layers) = maybe_layers { let color = if overlay
for i in (0..layers.visible.len()).rev() { .selection()
if layers.visible[i] { .contains_node(board, GenericNode::Primitive(primitive))
for primitive in board.layout().drawing().layer_primitive_nodes(i) { {
let shape = primitive.primitive(board.layout().drawing()).shape(); layers.highlight_colors[i]
} else if let Some(activity) = &mut workspace.maybe_activity {
let color = if overlay if activity.obstacles().contains(&primitive) {
.selection() layers.highlight_colors[i]
.contains_node(board, GenericNode::Primitive(primitive)) } else {
{ layers.colors[i]
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);
} }
} else {
layers.colors[i]
};
for poly in board.layout().layer_poly_nodes(i) { painter.paint_primitive(&shape, color);
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));
} }
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 let Some(navmesh) = activity.maybe_navmesh() {
if top.show_origin_destination { for edge in navmesh.edge_references() {
let (origin, destination) = (navmesh.origin(), navmesh.destination()); let mut from = PrimitiveIndex::from(navmesh.node_weight(edge.source()).unwrap().node)
painter.paint_dot( .primitive(board.layout().drawing())
Circle { .shape()
pos: board.layout().drawing().primitive(origin).shape().center(), .center();
r: 150.0, let mut to = PrimitiveIndex::from(navmesh.node_weight(edge.target()).unwrap().node)
}, .primitive(board.layout().drawing())
egui::Color32::from_rgb(255, 255, 100), .shape()
); .center();
painter.paint_dot(
Circle { if let Some(from_cw) = navmesh.node_weight(edge.source()).unwrap().maybe_cw {
pos: board.layout().drawing().primitive(destination).shape().center(), if from_cw {
r: 150.0, from -= [0.0, 150.0].into();
}, } else {
egui::Color32::from_rgb(255, 255, 100), 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 viewport_rect
@ -280,7 +259,7 @@ impl Viewport {
fn zoom_to_fit( fn zoom_to_fit(
&mut self, &mut self,
maybe_invoker: &mut Option<Invoker<SpecctraMesadata>>, maybe_invoker: Option<&mut Invoker<SpecctraMesadata>>,
viewport_rect: &egui::Rect, viewport_rect: &egui::Rect,
) { ) {
if self.scheduled_zoom_to_fit { if self.scheduled_zoom_to_fit {

View File

@ -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<Mutex<Invoker<SpecctraMesadata>>>,
pub maybe_activity: Option<ActivityStepperWithStatus>,
pub history_channel: (
Sender<std::io::Result<Result<History, serde_json::Error>>>,
Receiver<std::io::Result<Result<History, serde_json::Error>>>,
),
}
impl Workspace {
pub fn new(design: SpecctraDesign, tr: &Translator) -> Result<Self, String> {
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());
}
}