Add drag selection interactors

This commit is contained in:
Mikolaj Wielgus 2026-05-26 12:12:07 +02:00
parent 1b4bb49a89
commit 61c0134db4
8 changed files with 143 additions and 14 deletions

View File

@ -25,8 +25,9 @@ use topola::{
pub fn load_design(filename: &str) -> Autorouter<SpecctraMesadata> { pub fn load_design(filename: &str) -> Autorouter<SpecctraMesadata> {
let design_file = File::open(filename).unwrap(); let design_file = File::open(filename).unwrap();
let design_bufread = BufReader::new(design_file); let design_bufreader = BufReader::new(design_file);
let design = SpecctraDesign::load(design_bufread).unwrap(); let design = SpecctraDesign::load(design_bufreader).unwrap();
Autorouter::new(design.make_board(&mut BoardEdit::new())).unwrap() Autorouter::new(design.make_board(&mut BoardEdit::new())).unwrap()
} }

View File

@ -19,7 +19,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[dependencies] [dependencies]
derive-getters.workspace = true derive-getters.workspace = true
egui = "0.33" egui = { version = "0.33", features = ["serde"] }
eframe = { version = "0.33", default-features = false, features = [ eframe = { version = "0.33", default-features = false, features = [
#"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. #"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts. "default_fonts", # Embed the default egui fonts.

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
use egui::Pos2; use egui::Pos2;
use topola::Vector2; use topola::{CrossingDragSelectionInteractor, InteractiveInput, Vector2};
use crate::{display::Display, workspace::Workspace}; use crate::{display::Display, workspace::Workspace};
@ -11,6 +11,7 @@ pub struct Viewport {
pub scene_rect: egui::Rect, pub scene_rect: egui::Rect,
pub ref_scene_rect: egui::Rect, pub ref_scene_rect: egui::Rect,
pub scheduled_zoom_to_fit: bool, pub scheduled_zoom_to_fit: bool,
crossing_drag_selection_interactor: Option<CrossingDragSelectionInteractor>,
} }
impl Viewport { 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)), 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)), 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, scheduled_zoom_to_fit: false,
crossing_drag_selection_interactor: None,
} }
} }
@ -34,7 +36,7 @@ impl Viewport {
let response = egui::Scene::new() let response = egui::Scene::new()
.zoom_range(zoom_range.clone()) .zoom_range(zoom_range.clone())
//.sense(egui::Sense::hover()) .drag_pan_buttons(egui::DragPanButtons::MIDDLE)
.show(ui, &mut scene_rect, |ui| { .show(ui, &mut scene_rect, |ui| {
if let Some(ref workspace) = workspace { if let Some(ref workspace) = workspace {
let mut display = Display::new(); let mut display = Display::new();
@ -49,10 +51,35 @@ impl Viewport {
Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into()); Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into());
if let Some(workspace) = workspace { 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()) { 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_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 if let Some(pin_selector) = workspace
.autorouter .autorouter
.router() .router()
@ -60,10 +87,7 @@ impl Viewport {
.board() .board()
.locate_pin_at_point( .locate_pin_at_point(
workspace.appearance_panel.active, workspace.appearance_panel.active,
Vector2::new( pointer_scene,
pointer_scene_pos.x as i64,
pointer_scene_pos.y as i64,
),
) )
{ {
workspace.selection.pins.toggle(pin_selector); 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); self.zoom_to_fit_if_scheduled(workspace);
} }
}) })

View File

@ -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<i64>,
selection: PersistableSelection,
}
impl CrossingDragSelectionInteractor {
pub fn new(origin: Vector2<i64>) -> 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<i64>,
selection: PersistableSelection,
}
impl WindowDragSelectionInteractor {
pub fn new(origin: Vector2<i64>) -> 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);
}
}
}
}

View File

@ -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<i64>,
}
impl InteractiveInput {
pub fn new(pointer: Vector2<i64>) -> Self {
Self { pointer }
}
}

View File

@ -34,7 +34,7 @@ impl Board {
None None
} }
pub fn locate_component_intersecting_rect( pub fn locate_components_intersecting_rect(
&self, &self,
layer: LayerId, layer: LayerId,
rect: Rect2<i64>, rect: Rect2<i64>,
@ -59,7 +59,7 @@ impl Board {
) )
} }
pub fn locate_component_inside_rect( pub fn locate_components_inside_rect(
&self, &self,
layer: LayerId, layer: LayerId,
rect: Rect2<i64>, rect: Rect2<i64>,
@ -104,7 +104,7 @@ impl Board {
None None
} }
pub fn locate_pin_intersecting_rect( pub fn locate_pins_intersecting_rect(
&self, &self,
layer: LayerId, layer: LayerId,
rect: Rect2<i64>, rect: Rect2<i64>,
@ -129,7 +129,7 @@ impl Board {
) )
} }
pub fn locate_pin_inside_rect( pub fn locate_pins_inside_rect(
&self, &self,
layer: LayerId, layer: LayerId,
rect: Rect2<i64>, rect: Rect2<i64>,

View File

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
pub mod interactors;
mod layer; mod layer;
mod locate; mod locate;
mod resolve; mod resolve;

View File

@ -18,6 +18,7 @@ pub use crate::board::Board;
pub use crate::board::LayerDesc; pub use crate::board::LayerDesc;
pub use crate::board::LayerSide; pub use crate::board::LayerSide;
pub use crate::board::LayerType; pub use crate::board::LayerType;
pub use crate::board::interactors::{CrossingDragSelectionInteractor, InteractiveInput};
pub use crate::board::selections; pub use crate::board::selections;
pub use crate::layout::LayerId; pub use crate::layout::LayerId;
pub use crate::layout::Layout; pub use crate::layout::Layout;