diff --git a/topola-egui/src/app.rs b/topola-egui/src/app.rs index 1331393..e0201ca 100644 --- a/topola-egui/src/app.rs +++ b/topola-egui/src/app.rs @@ -8,7 +8,9 @@ use specctra::{error::ParseErrorContext, structure::DsnFile}; use topola::Board; use unic_langid::langid; -use crate::{menu_bar::MenuBar, translator::Translator, viewport::Viewport, workspace::Workspace}; +use crate::{ + menu_bar::MenuBar, translator::Translator, viewport::Viewport, workspace::GuiWorkspace, +}; pub struct App { translator: Translator, @@ -20,7 +22,7 @@ pub struct App { menu_bar: MenuBar, viewport: Viewport, - workspace: Option, + workspace: Option, } impl Default for App { @@ -50,7 +52,7 @@ impl App { fn update_state(&mut self) { if let Ok(data) = self.content_channel.1.try_recv() { - self.workspace = Some(Workspace::new( + self.workspace = Some(GuiWorkspace::new( Board::from_specctra(data.unwrap()), &self.translator, )); diff --git a/topola-egui/src/display.rs b/topola-egui/src/display.rs index 389ebc6..2c2347e 100644 --- a/topola-egui/src/display.rs +++ b/topola-egui/src/display.rs @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::{viewport::Viewport, workspace::Workspace}; -use topola::LayerId; +use crate::{viewport::Viewport, workspace::GuiWorkspace}; use topola::primitives::{Joint, Polygon, Segment, Via}; +use topola::{LayerId, Workspace}; pub struct Display {} @@ -19,7 +19,7 @@ impl Display { ui: &egui::Ui, //menu_bar: &MenuBar, viewport: &Viewport, - workspace: &Workspace, + workspace: &GuiWorkspace, ) { self.display_layout(ctx, ui, /*menu_bar,*/ viewport, workspace); self.display_bboxes(ctx, ui, viewport, workspace); @@ -33,9 +33,9 @@ impl Display { ui: &egui::Ui, //menu_bar: &MenuBar, viewport: &Viewport, - workspace: &Workspace, + workspace: &GuiWorkspace, ) { - let board = workspace.autorouter.router().navmesher_board().board(); + let board = workspace.workspace.board(); let layout = board.layout(); // Start from the bottom layer so that top layers are drawn on top. @@ -46,8 +46,10 @@ impl Display { for joint_id in layout.layer_joints(layer) { let joint = layout.joint(joint_id); - let pin_selected = board.pins_contain_joint(&workspace.selection.pins, joint_id); - let net_selected = board.nets_contain_joint(&workspace.selection.nets, joint_id); + let pin_selected = + board.pins_contain_joint(&workspace.workspace.selection().pins, joint_id); + let net_selected = + board.nets_contain_joint(&workspace.workspace.selection().nets, joint_id); self.paint_joint( ctx, ui, @@ -64,9 +66,9 @@ impl Display { for segment_id in layout.layer_segments(layer) { let segment = layout.segment(segment_id); let pin_selected = - board.pins_contain_segment(&workspace.selection.pins, segment_id); + board.pins_contain_segment(&workspace.workspace.selection().pins, segment_id); let net_selected = - board.nets_contain_segment(&workspace.selection.nets, segment_id); + board.nets_contain_segment(&workspace.workspace.selection().nets, segment_id); self.paint_segment( ctx, ui, @@ -82,8 +84,10 @@ impl Display { for via_id in layout.layer_vias(layer) { let via = layout.via(via_id); - let pin_selected = board.pins_contain_via(&workspace.selection.pins, via_id); - let net_selected = board.nets_contain_via(&workspace.selection.nets, via_id); + let pin_selected = + board.pins_contain_via(&workspace.workspace.selection().pins, via_id); + let net_selected = + board.nets_contain_via(&workspace.workspace.selection().nets, via_id); self.paint_via( ctx, ui, @@ -100,9 +104,9 @@ impl Display { for polygon_id in layout.layer_polygons(layer) { let polygon = layout.polygon(polygon_id); let pin_selected = - board.pins_contain_polygon(&workspace.selection.pins, polygon_id); + board.pins_contain_polygon(&workspace.workspace.selection().pins, polygon_id); let net_selected = - board.nets_contain_polygon(&workspace.selection.nets, polygon_id); + board.nets_contain_polygon(&workspace.workspace.selection().nets, polygon_id); self.paint_polygon( ctx, ui, @@ -203,9 +207,9 @@ impl Display { ctx: &egui::Context, ui: &egui::Ui, viewport: &Viewport, - workspace: &Workspace, + workspace: &GuiWorkspace, ) { - let board = workspace.autorouter.router().navmesher_board().board(); + let board = workspace.workspace.board(); let layout = board.layout(); for layer in (0..*layout.layer_count()).rev().map(LayerId::new) { @@ -283,20 +287,16 @@ impl Display { ctx: &egui::Context, ui: &egui::Ui, viewport: &Viewport, - workspace: &Workspace, + workspace: &GuiWorkspace, ) { - for layer in (0..*workspace - .autorouter - .router() - .navmesher_board() - .board() - .layout() - .layer_count()) - .map(LayerId::new) - { + let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else { + return; + }; + let autorouter = &autorouter_workspace.autorouter; + + for layer in (0..*workspace.workspace.board().layout().layer_count()).map(LayerId::new) { if workspace.appearance_panel.visible[layer.index()] { - for navmesh in workspace - .autorouter + for navmesh in autorouter .router() .navmesher_board() .navmesher() @@ -354,9 +354,14 @@ impl Display { _ctx: &egui::Context, ui: &egui::Ui, _viewport: &Viewport, - workspace: &Workspace, + workspace: &GuiWorkspace, ) { - for ratline in workspace.autorouter.ratsnest().ratlines() { + let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else { + return; + }; + let autorouter = &autorouter_workspace.autorouter; + + for ratline in autorouter.ratsnest().ratlines() { let layers = *ratline.endpoint_layers(); let endpoints = *ratline.endpoints(); diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index 192d82d..6864127 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -3,15 +3,15 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use egui::Pos2; -use topola::{InteractiveInput, SelectionCombineMode, SelectionInteractor, Vector2}; +use topola::{InteractiveInput, MasterInteractor, Vector2, Workspace}; -use crate::{display::Display, workspace::Workspace}; +use crate::{display::Display, workspace::GuiWorkspace}; pub struct Viewport { pub scene_rect: egui::Rect, pub ref_scene_rect: egui::Rect, pub scheduled_zoom_to_fit: bool, - selection_interactor: Option, + master_interactor: Option, } impl Viewport { @@ -20,11 +20,11 @@ impl Viewport { scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)), ref_scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)), scheduled_zoom_to_fit: false, - selection_interactor: None, + master_interactor: None, } } - pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut Workspace>) { + pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut GuiWorkspace>) { egui::CentralPanel::default().show(ctx, |ui| { egui::Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); @@ -52,7 +52,7 @@ impl Viewport { if let Some(workspace) = workspace { if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { - self.selection_interactor = None; + self.master_interactor = None; } let primary_pressed = @@ -61,6 +61,7 @@ impl Viewport { ctx.input(|i| i.pointer.button_down(egui::PointerButton::Primary)); let primary_released = ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary)); + let delete_pressed = ctx.input(|i| i.key_pressed(egui::Key::Delete)); let mut maybe_pointer_on_scene: Option> = None; if let Some(pointer_viewport_pos) = ctx.input(|i| i.pointer.interact_pos()) { @@ -73,17 +74,20 @@ impl Viewport { maybe_pointer_on_scene = Some(pointer_on_scene); if primary_pressed && response.hovered() { - self.selection_interactor = Some(SelectionInteractor::new( - pointer_on_scene, - workspace.selection.clone(), - SelectionCombineMode::Replace, + self.master_interactor = Some(MasterInteractor::new( + None, + workspace.workspace.selection().clone(), )); } - if let Some(interactor) = self.selection_interactor.as_mut() { + if let Some(interactor) = self.master_interactor.as_mut() { if primary_down { - let _ = interactor.update( - workspace.autorouter.router().navmesher_board().board(), + let board = match &mut workspace.workspace { + Workspace::Board(workspace) => &mut workspace.board, + Workspace::Autorouter(_) => panic!("expected board workspace"), + }; + interactor.update( + board, workspace.appearance_panel.active, InteractiveInput::new(pointer_on_scene, false, false, false), ); @@ -92,19 +96,45 @@ impl Viewport { } if primary_released { - if let Some(mut interactor) = self.selection_interactor.take() { - let pointer_for_scene = - maybe_pointer_on_scene.unwrap_or(*interactor.origin()); - let _ = interactor.update( - workspace.autorouter.router().navmesher_board().board(), + if let Some(mut interactor) = self.master_interactor.take() { + let pointer_for_scene = maybe_pointer_on_scene.unwrap_or_else(|| { + interactor + .selection_interactor() + .as_ref() + .map(|selection_interactor| *selection_interactor.origin()) + .unwrap_or(Vector2::new(0, 0)) + }); + let board = match &mut workspace.workspace { + Workspace::Board(workspace) => &mut workspace.board, + Workspace::Autorouter(_) => panic!("expected board workspace"), + }; + interactor.update( + board, workspace.appearance_panel.active, InteractiveInput::new(pointer_for_scene, true, false, false), ); - workspace.selection = interactor.selection().clone(); + *workspace.workspace.selection_mut() = interactor.selection().clone(); } } + if delete_pressed { + let pointer_for_scene = + maybe_pointer_on_scene.unwrap_or(Vector2::new(0, 0)); + let mut interactor = + MasterInteractor::new(None, workspace.workspace.selection().clone()); + let board = match &mut workspace.workspace { + Workspace::Board(workspace) => &mut workspace.board, + Workspace::Autorouter(_) => panic!("expected board workspace"), + }; + interactor.update( + board, + workspace.appearance_panel.active, + InteractiveInput::new(pointer_for_scene, false, true, false), + ); + *workspace.workspace.selection_mut() = interactor.selection().clone(); + } + self.zoom_to_fit_if_scheduled(workspace); } }) @@ -141,7 +171,7 @@ impl Viewport { * egui::emath::TSTransform::from_scaling(scale) } - fn zoom_to_fit_if_scheduled(&mut self, workspace: &Workspace) { + fn zoom_to_fit_if_scheduled(&mut self, workspace: &GuiWorkspace) { if self.scheduled_zoom_to_fit { self.scene_rect = Self::boundary_bounding_box(workspace); self.ref_scene_rect = self.scene_rect.clone(); @@ -150,29 +180,15 @@ impl Viewport { self.scheduled_zoom_to_fit = false; } - fn boundary_bounding_box(workspace: &Workspace) -> egui::Rect { - let first = workspace - .autorouter - .router() - .navmesher_board() - .board() - .layout() - .boundary()[0]; + fn boundary_bounding_box(workspace: &GuiWorkspace) -> egui::Rect { + let first = workspace.workspace.board().layout().boundary()[0]; let mut min_x = first[0]; let mut max_x = first[0]; let mut min_y = first[1]; let mut max_y = first[1]; - for point in workspace - .autorouter - .router() - .navmesher_board() - .board() - .layout() - .boundary()[1..] - .iter() - { + for point in workspace.workspace.board().layout().boundary()[1..].iter() { if point[0] < min_x { min_x = point[0]; } diff --git a/topola-egui/src/workspace.rs b/topola-egui/src/workspace.rs index 59f5992..80601c8 100644 --- a/topola-egui/src/workspace.rs +++ b/topola-egui/src/workspace.rs @@ -2,29 +2,30 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 -use topola::{Autorouter, Board, selections::PersistableSelection}; +use topola::{Board, Workspace}; use crate::{layers_panel::LayersPanel, translator::Translator}; -pub struct Workspace { - pub autorouter: Autorouter, +pub struct GuiWorkspace { + pub workspace: Workspace, pub appearance_panel: LayersPanel, - pub selection: PersistableSelection, } -impl Workspace { +impl GuiWorkspace { pub fn new(board: Board, tr: &Translator) -> Self { let appearance_panel = LayersPanel::new(&board); Self { - autorouter: Autorouter::new(board), + workspace: Workspace::new_board(board), appearance_panel, - selection: PersistableSelection::new(), } } pub fn update_appearance_panel(&mut self, ctx: &egui::Context) { - self.appearance_panel - .update(ctx, &self.autorouter.router().navmesher_board().board()); + let Self { + workspace, + appearance_panel, + } = self; + appearance_panel.update(ctx, workspace.board()); } } diff --git a/topola/src/board/interactors/drag_selection.rs b/topola/src/board/interactors/drag_selection.rs index 0b57053..b5de923 100644 --- a/topola/src/board/interactors/drag_selection.rs +++ b/topola/src/board/interactors/drag_selection.rs @@ -43,14 +43,10 @@ impl DragSelectionInteractor { } } - pub fn update( - &mut self, - board: &Board, - input: InteractiveInput, - ) -> Option { + pub fn update(&mut self, board: &Board, input: InteractiveInput) { if input.cancel { self.selection = self.original_selection.clone(); - return Some(self.selection.clone()); + return; } self.selection = PersistableSelection::new(); @@ -141,6 +137,5 @@ impl DragSelectionInteractor { } self.selection = combined_selection; - Some(self.selection.clone()) } } diff --git a/topola/src/board/interactors/master.rs b/topola/src/board/interactors/master.rs index b688907..ede432f 100644 --- a/topola/src/board/interactors/master.rs +++ b/topola/src/board/interactors/master.rs @@ -17,12 +17,12 @@ use crate::{ #[derive(Clone, Constructor, Debug, Eq, Getters, PartialEq)] pub struct MasterInteractor { - selection: PersistableSelection, selection_interactor: Option, + selection: PersistableSelection, } impl MasterInteractor { - pub fn update(&mut self, board: &mut Board, input: InteractiveInput) { + pub fn update(&mut self, board: &mut Board, layer: LayerId, input: InteractiveInput) { if input.delete { board.delete_net_free_primitives(self.selection.nets.clone()); } @@ -31,16 +31,13 @@ impl MasterInteractor { self.selection_interactor = Some(SelectionInteractor::new( input.pointer, self.selection.clone(), - SelectionCombineMode::Additive, + SelectionCombineMode::Replace, )); } if let Some(selection_interactor) = self.selection_interactor.as_mut() { - if let Some(selection) = - selection_interactor.update(board, LayerId::new(0), input.clone()) - { - self.selection = selection; - } + selection_interactor.update(board, layer, input.clone()); + self.selection = selection_interactor.selection().clone(); } if input.release || input.cancel { diff --git a/topola/src/board/interactors/mod.rs b/topola/src/board/interactors/mod.rs index ccb19f7..e9b80d7 100644 --- a/topola/src/board/interactors/mod.rs +++ b/topola/src/board/interactors/mod.rs @@ -8,6 +8,7 @@ mod selection; use derive_more::Constructor; pub use drag_selection::{DragSelectionInteractor, DragSelectionOptions}; +pub use master::MasterInteractor; pub use selection::SelectionInteractor; use serde::{Deserialize, Serialize}; diff --git a/topola/src/board/interactors/selection.rs b/topola/src/board/interactors/selection.rs index 5b60e06..4fe6aa9 100644 --- a/topola/src/board/interactors/selection.rs +++ b/topola/src/board/interactors/selection.rs @@ -39,15 +39,10 @@ impl SelectionInteractor { } } - pub fn update( - &mut self, - board: &Board, - layer: LayerId, - input: InteractiveInput, - ) -> Option { + pub fn update(&mut self, board: &Board, layer: LayerId, input: InteractiveInput) { if input.cancel { self.selection = self.original_selection.clone(); - return Some(self.selection.clone()); + return; } if input.release && input.pointer == self.origin { @@ -76,8 +71,8 @@ impl SelectionInteractor { .xor(std::iter::once(component_selector)); } - self.selection = selection.clone(); - return Some(selection); + self.selection = selection; + return; } let contain = if input.pointer.x >= self.origin.x { @@ -89,9 +84,7 @@ impl SelectionInteractor { let options = DragSelectionOptions::new(self.combine.clone(), contain); let mut drag_selection_interactor = DragSelectionInteractor::new(self.origin, self.original_selection.clone(), options); - let selection = drag_selection_interactor.update(board, input)?; - - self.selection = selection.clone(); - Some(selection) + drag_selection_interactor.update(board, input); + self.selection = drag_selection_interactor.selection().clone(); } } diff --git a/topola/src/board/transforms/delete.rs b/topola/src/board/transforms/delete.rs index 8186fff..4919097 100644 --- a/topola/src/board/transforms/delete.rs +++ b/topola/src/board/transforms/delete.rs @@ -8,6 +8,7 @@ impl Board { pub fn delete_net_free_primitives(&mut self, selection: NetSelection) { for joint_id in self .resolve_net_joints(selection.clone()) + .filter(|&joint_id| self.layout.joint(joint_id).spec.pin.is_none()) .collect::>() .clone() { @@ -16,6 +17,7 @@ impl Board { for segment_id in self .resolve_net_segments(selection.clone()) + .filter(|&segment_id| self.layout.segment(segment_id).spec.pin.is_none()) .collect::>() .clone() { @@ -24,6 +26,7 @@ impl Board { for via_id in self .resolve_net_vias(selection.clone()) + .filter(|&via_id| self.layout.via(via_id).spec.pin.is_none()) .collect::>() .clone() { @@ -32,6 +35,7 @@ impl Board { for polygon_id in self .resolve_net_polygons(selection.clone()) + .filter(|&polygon_id| self.layout.polygon(polygon_id).pin.is_none()) .collect::>() .clone() { diff --git a/topola/src/lib.rs b/topola/src/lib.rs index 72f592b..e7195cc 100644 --- a/topola/src/lib.rs +++ b/topola/src/lib.rs @@ -12,6 +12,7 @@ mod pathfinder; mod ratsnest; mod router; mod specctra; +mod workspace; pub use crate::autorouter::Autorouter; pub use crate::board::Board; @@ -19,8 +20,8 @@ pub use crate::board::LayerDesc; pub use crate::board::LayerSide; pub use crate::board::LayerType; pub use crate::board::interactors::{ - DragSelectionInteractor, DragSelectionOptions, InteractiveInput, SelectionCombineMode, - SelectionContainMode, SelectionInteractor, + DragSelectionInteractor, DragSelectionOptions, InteractiveInput, MasterInteractor, + SelectionCombineMode, SelectionContainMode, SelectionInteractor, }; pub use crate::board::selections; pub use crate::layout::LayerId; @@ -29,3 +30,4 @@ pub use crate::layout::compounds::{Pin, PinId}; pub use crate::layout::primitives; pub use crate::math::{Rect2, Rect3, Vector2, Vector3}; pub use crate::ratsnest::{Ratline, Ratsnest}; +pub use crate::workspace::{AutorouterWorkspace, BoardWorkspace, Workspace}; diff --git a/topola/src/workspace.rs b/topola/src/workspace.rs new file mode 100644 index 0000000..596952d --- /dev/null +++ b/topola/src/workspace.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::{Autorouter, Board, selections::PersistableSelection}; + +pub enum Workspace { + Board(BoardWorkspace), + Autorouter(AutorouterWorkspace), +} + +impl Workspace { + pub fn new_board(board: Board) -> Self { + Self::Board(BoardWorkspace::new(board)) + } + + pub fn new_autorouter(board: Board) -> Self { + Self::Autorouter(AutorouterWorkspace::new(board)) + } + + pub fn selection(&self) -> &PersistableSelection { + match self { + Workspace::Board(workspace) => &workspace.selection, + Workspace::Autorouter(workspace) => &workspace.selection, + } + } + + pub fn selection_mut(&mut self) -> &mut PersistableSelection { + match self { + Workspace::Board(workspace) => &mut workspace.selection, + Workspace::Autorouter(workspace) => &mut workspace.selection, + } + } + + pub fn board(&self) -> &Board { + match self { + Workspace::Board(workspace) => &workspace.board, + Workspace::Autorouter(workspace) => { + workspace.autorouter.router().navmesher_board().board() + } + } + } +} + +pub struct BoardWorkspace { + pub board: Board, + pub selection: PersistableSelection, +} + +impl BoardWorkspace { + pub fn new(board: Board) -> Self { + Self { + board, + selection: PersistableSelection::new(), + } + } +} + +pub struct AutorouterWorkspace { + pub autorouter: Autorouter, + pub selection: PersistableSelection, +} + +impl AutorouterWorkspace { + pub fn new(board: Board) -> Self { + Self { + autorouter: Autorouter::new(board), + selection: PersistableSelection::new(), + } + } +}