diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2aa9f6c..cc8c919 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -25,8 +25,9 @@ use topola::{ pub fn load_design(filename: &str) -> Autorouter { let design_file = File::open(filename).unwrap(); - let design_bufread = BufReader::new(design_file); - let design = SpecctraDesign::load(design_bufread).unwrap(); + let design_bufreader = BufReader::new(design_file); + let design = SpecctraDesign::load(design_bufreader).unwrap(); + Autorouter::new(design.make_board(&mut BoardEdit::new())).unwrap() } diff --git a/topola-egui/Cargo.toml b/topola-egui/Cargo.toml index bcbadd1..9a6e6be 100644 --- a/topola-egui/Cargo.toml +++ b/topola-egui/Cargo.toml @@ -19,7 +19,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] derive-getters.workspace = true -egui = "0.33" +egui = { version = "0.33", features = ["serde"] } eframe = { version = "0.33", default-features = false, features = [ #"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. "default_fonts", # Embed the default egui fonts. diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index 2ea7f37..436ee18 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use egui::Pos2; -use topola::Vector2; +use topola::{CrossingDragSelectionInteractor, InteractiveInput, Vector2}; use crate::{display::Display, workspace::Workspace}; @@ -11,6 +11,7 @@ pub struct Viewport { pub scene_rect: egui::Rect, pub ref_scene_rect: egui::Rect, pub scheduled_zoom_to_fit: bool, + crossing_drag_selection_interactor: Option, } impl Viewport { @@ -19,6 +20,7 @@ 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, + crossing_drag_selection_interactor: None, } } @@ -34,7 +36,7 @@ impl Viewport { let response = egui::Scene::new() .zoom_range(zoom_range.clone()) - //.sense(egui::Sense::hover()) + .drag_pan_buttons(egui::DragPanButtons::MIDDLE) .show(ui, &mut scene_rect, |ui| { if let Some(ref workspace) = workspace { let mut display = Display::new(); @@ -49,10 +51,35 @@ impl Viewport { Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into()); if let Some(workspace) = workspace { + if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { + self.crossing_drag_selection_interactor = None; + } + + let primary_pressed = + ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary)); + let primary_down = + ctx.input(|i| i.pointer.button_down(egui::PointerButton::Primary)); + let primary_released = + ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary)); + if let Some(pointer_viewport_pos) = ctx.input(|i| i.pointer.interact_pos()) { let pointer_scene_pos = scene_to_viewport.inverse() * pointer_viewport_pos; + let pointer_scene = + Vector2::new(pointer_scene_pos.x as i64, pointer_scene_pos.y as i64); - if response.clicked() { + if primary_pressed && response.hovered() { + self.crossing_drag_selection_interactor = + Some(CrossingDragSelectionInteractor::new(pointer_scene)); + } + + if let Some(interactor) = self.crossing_drag_selection_interactor.as_mut() { + if primary_down || primary_released { + interactor.update( + workspace.autorouter.router().navmesher_board().board(), + InteractiveInput::new(pointer_scene), + ); + } + } else if response.clicked() { if let Some(pin_selector) = workspace .autorouter .router() @@ -60,10 +87,7 @@ impl Viewport { .board() .locate_pin_at_point( workspace.appearance_panel.active, - Vector2::new( - pointer_scene_pos.x as i64, - pointer_scene_pos.y as i64, - ), + pointer_scene, ) { workspace.selection.pins.toggle(pin_selector); @@ -71,6 +95,12 @@ impl Viewport { } } + if primary_released { + if let Some(interactor) = self.crossing_drag_selection_interactor.take() { + workspace.selection = interactor.selection().clone(); + } + } + self.zoom_to_fit_if_scheduled(workspace); } }) diff --git a/topola/src/board/interactors/drag_selection.rs b/topola/src/board/interactors/drag_selection.rs new file mode 100644 index 0000000..266f134 --- /dev/null +++ b/topola/src/board/interactors/drag_selection.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use derive_getters::Getters; + +use crate::{ + Rect2, Vector2, + board::{Board, interactors::InteractiveInput, selections::PersistableSelection}, + layout::LayerId, +}; + +#[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)] +pub struct CrossingDragSelectionInteractor { + origin: Vector2, + selection: PersistableSelection, +} + +impl CrossingDragSelectionInteractor { + pub fn new(origin: Vector2) -> Self { + Self { + origin, + selection: PersistableSelection::new(), + } + } + + pub fn update(&mut self, board: &Board, input: InteractiveInput) { + let rect = Rect2::new(self.origin, input.pointer); + + self.selection = PersistableSelection::new(); + + for layer_index in 0..*board.layout().layer_count() { + let layer = LayerId::new(layer_index); + + for selector in board.locate_components_intersecting_rect(layer, rect) { + self.selection.components.0.insert(selector); + } + + for selector in board.locate_pins_intersecting_rect(layer, rect) { + self.selection.pins.0.insert(selector); + } + } + } +} + +#[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)] +pub struct WindowDragSelectionInteractor { + origin: Vector2, + selection: PersistableSelection, +} + +impl WindowDragSelectionInteractor { + pub fn new(origin: Vector2) -> Self { + Self { + origin, + selection: PersistableSelection::new(), + } + } + + pub fn update(&mut self, board: &Board, input: InteractiveInput) { + let rect = Rect2::new(self.origin, input.pointer); + + self.selection = PersistableSelection::new(); + + for layer_index in 0..*board.layout().layer_count() { + let layer = LayerId::new(layer_index); + + for selector in board.locate_components_inside_rect(layer, rect) { + self.selection.components.0.insert(selector); + } + + for selector in board.locate_pins_inside_rect(layer, rect) { + self.selection.pins.0.insert(selector); + } + } + } +} diff --git a/topola/src/board/interactors/mod.rs b/topola/src/board/interactors/mod.rs new file mode 100644 index 0000000..5077245 --- /dev/null +++ b/topola/src/board/interactors/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod drag_selection; + +pub use drag_selection::CrossingDragSelectionInteractor; + +use crate::Vector2; + +pub struct InteractiveInput { + pointer: Vector2, +} + +impl InteractiveInput { + pub fn new(pointer: Vector2) -> Self { + Self { pointer } + } +} diff --git a/topola/src/board/locate.rs b/topola/src/board/locate.rs index 7438d90..b51d949 100644 --- a/topola/src/board/locate.rs +++ b/topola/src/board/locate.rs @@ -34,7 +34,7 @@ impl Board { None } - pub fn locate_component_intersecting_rect( + pub fn locate_components_intersecting_rect( &self, layer: LayerId, rect: Rect2, @@ -59,7 +59,7 @@ impl Board { ) } - pub fn locate_component_inside_rect( + pub fn locate_components_inside_rect( &self, layer: LayerId, rect: Rect2, @@ -104,7 +104,7 @@ impl Board { None } - pub fn locate_pin_intersecting_rect( + pub fn locate_pins_intersecting_rect( &self, layer: LayerId, rect: Rect2, @@ -129,7 +129,7 @@ impl Board { ) } - pub fn locate_pin_inside_rect( + pub fn locate_pins_inside_rect( &self, layer: LayerId, rect: Rect2, diff --git a/topola/src/board/mod.rs b/topola/src/board/mod.rs index 9e38cb7..9f4e2ce 100644 --- a/topola/src/board/mod.rs +++ b/topola/src/board/mod.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 +pub mod interactors; mod layer; mod locate; mod resolve; diff --git a/topola/src/lib.rs b/topola/src/lib.rs index c1d7739..11cd4f9 100644 --- a/topola/src/lib.rs +++ b/topola/src/lib.rs @@ -18,6 +18,7 @@ pub use crate::board::Board; pub use crate::board::LayerDesc; pub use crate::board::LayerSide; pub use crate::board::LayerType; +pub use crate::board::interactors::{CrossingDragSelectionInteractor, InteractiveInput}; pub use crate::board::selections; pub use crate::layout::LayerId; pub use crate::layout::Layout;