diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index 519ffab..b3f4295 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -3,10 +3,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use egui::Pos2; -use topola::{ - DragSelectionInteractor, InteractiveInput, SelectionCombineMode, SelectionContainMode, - SelectionOptions, Vector2, Vector3, -}; +use topola::{InteractiveInput, SelectionCombineMode, SelectionInteractor, Vector2}; use crate::{display::Display, workspace::Workspace}; @@ -14,7 +11,7 @@ pub struct Viewport { pub scene_rect: egui::Rect, pub ref_scene_rect: egui::Rect, pub scheduled_zoom_to_fit: bool, - drag_selection_interactor: Option, + selection_interactor: Option, } impl Viewport { @@ -23,7 +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, - drag_selection_interactor: None, + selection_interactor: None, } } @@ -55,7 +52,7 @@ impl Viewport { if let Some(workspace) = workspace { if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { - self.drag_selection_interactor = None; + self.selection_interactor = None; } let primary_pressed = @@ -64,49 +61,46 @@ 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 mut maybe_pointer_on_scene: Option> = None; 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); + let pointer_on_scene_pos = + scene_to_viewport.inverse() * pointer_viewport_pos; + let pointer_on_scene = Vector2::new( + pointer_on_scene_pos.x as i64, + pointer_on_scene_pos.y as i64, + ); + maybe_pointer_on_scene = Some(pointer_on_scene); if primary_pressed && response.hovered() { - self.drag_selection_interactor = Some(DragSelectionInteractor::new( - pointer_scene, + self.selection_interactor = Some(SelectionInteractor::new( + pointer_on_scene, workspace.selection.clone(), - SelectionOptions::new( - SelectionCombineMode::Replace, - SelectionContainMode::Crossing, - ), + SelectionCombineMode::Replace, )); } - if response.clicked() { - if let Some(pin_selector) = workspace - .autorouter - .router() - .navmesher_board() - .board() - .locate_pin_at_point(Vector3::new( - pointer_scene.x, - pointer_scene.y, - workspace.appearance_panel.active.index() as i64, - )) - { - workspace.selection.pins.xor(std::iter::once(pin_selector)); - } - } else if let Some(interactor) = self.drag_selection_interactor.as_mut() { - if primary_down || primary_released { + if let Some(interactor) = self.selection_interactor.as_mut() { + if primary_down { let _ = interactor.update( workspace.autorouter.router().navmesher_board().board(), - InteractiveInput::new(pointer_scene, false), + workspace.appearance_panel.active, + InteractiveInput::new(pointer_on_scene, false, false), ); } } } if primary_released { - if let Some(interactor) = self.drag_selection_interactor.take() { + 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(), + workspace.appearance_panel.active, + InteractiveInput::new(pointer_for_scene, true, false), + ); + workspace.selection = interactor.selection().clone(); } } diff --git a/topola/src/board/interactors/drag_selection.rs b/topola/src/board/interactors/drag_selection.rs index 96aa893..d7bc3b6 100644 --- a/topola/src/board/interactors/drag_selection.rs +++ b/topola/src/board/interactors/drag_selection.rs @@ -3,31 +3,37 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use derive_getters::Getters; +use derive_more::Constructor; +use serde::{Deserialize, Serialize}; use crate::{ Rect3, Vector2, Vector3, board::{ Board, - interactors::{ - InteractiveInput, SelectionCombineMode, SelectionContainMode, SelectionOptions, - }, + interactors::{InteractiveInput, SelectionCombineMode, SelectionContainMode}, selections::PersistableSelection, }, }; +#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct DragSelectionOptions { + combine: SelectionCombineMode, + contain: SelectionContainMode, +} + #[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)] pub struct DragSelectionInteractor { origin: Vector2, original_selection: PersistableSelection, selection: PersistableSelection, - options: SelectionOptions, + options: DragSelectionOptions, } impl DragSelectionInteractor { pub fn new( origin: Vector2, original_selection: PersistableSelection, - options: SelectionOptions, + options: DragSelectionOptions, ) -> Self { Self { origin, diff --git a/topola/src/board/interactors/mod.rs b/topola/src/board/interactors/mod.rs index 81ae521..54e5f9e 100644 --- a/topola/src/board/interactors/mod.rs +++ b/topola/src/board/interactors/mod.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 mod drag_selection; +mod selection; -use derive_more::Constructor; -pub use drag_selection::DragSelectionInteractor; +pub use drag_selection::{DragSelectionInteractor, DragSelectionOptions}; +pub use selection::SelectionInteractor; use serde::{Deserialize, Serialize}; use crate::Vector2; @@ -13,12 +14,17 @@ use crate::Vector2; #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct InteractiveInput { pointer: Vector2, + released: bool, cancel: bool, } impl InteractiveInput { - pub fn new(pointer: Vector2, cancel: bool) -> Self { - Self { pointer, cancel } + pub fn new(pointer: Vector2, released: bool, cancel: bool) -> Self { + Self { + pointer, + released, + cancel, + } } } @@ -35,9 +41,3 @@ pub enum SelectionContainMode { Crossing, Window, } - -#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] -pub struct SelectionOptions { - combine: SelectionCombineMode, - contain: SelectionContainMode, -} diff --git a/topola/src/board/interactors/selection.rs b/topola/src/board/interactors/selection.rs new file mode 100644 index 0000000..fd86807 --- /dev/null +++ b/topola/src/board/interactors/selection.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use derive_getters::Getters; + +use crate::{ + Vector2, Vector3, + board::{ + Board, + interactors::{ + DragSelectionInteractor, DragSelectionOptions, InteractiveInput, SelectionCombineMode, + SelectionContainMode, + }, + selections::PersistableSelection, + }, + layout::LayerId, +}; + +#[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)] +pub struct SelectionInteractor { + origin: Vector2, + original_selection: PersistableSelection, + selection: PersistableSelection, + combine: SelectionCombineMode, +} + +impl SelectionInteractor { + pub fn new( + origin: Vector2, + original_selection: PersistableSelection, + combine: SelectionCombineMode, + ) -> Self { + Self { + origin, + original_selection, + selection: PersistableSelection::new(), + combine, + } + } + + pub fn update( + &mut self, + board: &Board, + layer: LayerId, + input: InteractiveInput, + ) -> Option { + if input.cancel { + self.selection = self.original_selection.clone(); + return Some(self.selection.clone()); + } + + if input.released && input.pointer == self.origin { + let mut selection = self.original_selection.clone(); + + // Pins have intentional precedence over nets and components. + if let Some(pin_selector) = board.locate_pin_at_point(Vector3::new( + input.pointer.x, + input.pointer.y, + layer.index() as i64, + )) { + selection.pins.xor(std::iter::once(pin_selector)); + } else if let Some(net_selector) = board.locate_net_at_point(Vector3::new( + input.pointer.x, + input.pointer.y, + layer.index() as i64, + )) { + selection.nets.xor(std::iter::once(net_selector)); + } else if let Some(component_selector) = board.locate_component_at_point(Vector3::new( + input.pointer.x, + input.pointer.y, + layer.index() as i64, + )) { + selection + .components + .xor(std::iter::once(component_selector)); + } + + self.selection = selection.clone(); + return Some(selection); + } + + let contain = if input.pointer.x >= self.origin.x { + SelectionContainMode::Window + } else { + SelectionContainMode::Crossing + }; + + 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) + } +} diff --git a/topola/src/board/locate.rs b/topola/src/board/locate.rs index 554ee8c..47104b1 100644 --- a/topola/src/board/locate.rs +++ b/topola/src/board/locate.rs @@ -79,21 +79,24 @@ impl Board { ) } - pub fn locate_nets_at_point( - &self, - point: Vector3, - ) -> impl Iterator + '_ { - let mut selectors = BTreeSet::new(); - - for net_id in self.layout.locate_nets_at_point(point) { - let Some(net_name) = self.net_name(net_id) else { - continue; - }; - - selectors.insert(NetSelector::new(net_name.to_string())); + pub fn locate_net_at_point(&self, point: Vector3) -> Option { + if let Some(joint_id) = self.layout.locate_joints_at_point(point).next() { + return self.joint_net_selector(joint_id); } - selectors.into_iter() + if let Some(segment_id) = self.layout.locate_segments_at_point(point).next() { + return self.segment_net_selector(segment_id); + } + + if let Some(via_id) = self.layout.locate_vias_at_point(point).next() { + return self.via_net_selector(via_id); + } + + if let Some(polygon_id) = self.layout.locate_polygons_at_point(point).next() { + return self.polygon_net_selector(polygon_id); + } + + None } pub fn locate_nets_intersecting_rect( @@ -131,6 +134,11 @@ impl Board { } pub fn locate_pin_at_point(&self, point: Vector3) -> Option { + // Polygons have intentional precedence for pins. + if let Some(polygon_id) = self.layout.locate_polygons_at_point(point).next() { + return self.polygon_pin_selector(polygon_id); + } + if let Some(joint_id) = self.layout.locate_joints_at_point(point).next() { return self.joint_pin_selector(joint_id); } @@ -143,10 +151,6 @@ impl Board { return self.via_pin_selector(via_id); } - if let Some(polygon_id) = self.layout.locate_polygons_at_point(point).next() { - return self.polygon_pin_selector(polygon_id); - } - None } diff --git a/topola/src/board/select.rs b/topola/src/board/select.rs index 0c03253..c2bc3bf 100644 --- a/topola/src/board/select.rs +++ b/topola/src/board/select.rs @@ -194,6 +194,38 @@ impl Board { .contains(&NetSelector::new(net_name.to_string())) } + pub fn joint_net_selector(&self, id: JointId) -> Option { + let joint = self.layout.joint(id); + + Some(NetSelector { + net: self.net_name(joint.spec.net)?.to_string(), + }) + } + + pub fn segment_net_selector(&self, id: SegmentId) -> Option { + let segment = self.layout.segment(id); + + Some(NetSelector { + net: self.net_name(segment.net)?.to_string(), + }) + } + + pub fn via_net_selector(&self, id: ViaId) -> Option { + let via = self.layout.via(id); + + Some(NetSelector { + net: self.net_name(via.net)?.to_string(), + }) + } + + pub fn polygon_net_selector(&self, id: PolygonId) -> Option { + let polygon = self.layout.polygon(id); + + Some(NetSelector { + net: self.net_name(polygon.net)?.to_string(), + }) + } + pub fn pins_contain_joint(&self, selection: &PinSelection, id: JointId) -> bool { let Some(selector) = self.joint_pin_selector(id) else { return false; diff --git a/topola/src/layout/locate.rs b/topola/src/layout/locate.rs index c4e338c..8a876ed 100644 --- a/topola/src/layout/locate.rs +++ b/topola/src/layout/locate.rs @@ -121,28 +121,6 @@ impl Layout { .map(|geom_with_data| geom_with_data.data) } - pub fn locate_nets_at_point(&self, point: Vector3) -> impl Iterator { - let mut nets = BTreeSet::new(); - - for joint_id in self.locate_joints_at_point(point) { - nets.insert(self.joint(joint_id).spec.net); - } - - for segment_id in self.locate_segments_at_point(point) { - nets.insert(self.segment(segment_id).net); - } - - for via_id in self.locate_vias_at_point(point) { - nets.insert(self.via(via_id).net); - } - - for polygon_id in self.locate_polygons_at_point(point) { - nets.insert(self.polygon(polygon_id).net); - } - - nets.into_iter() - } - pub fn locate_nets_intersecting_rect(&self, rect: Rect3) -> impl Iterator { let mut nets = BTreeSet::new(); diff --git a/topola/src/lib.rs b/topola/src/lib.rs index 8349da7..72f592b 100644 --- a/topola/src/lib.rs +++ b/topola/src/lib.rs @@ -19,8 +19,8 @@ pub use crate::board::LayerDesc; pub use crate::board::LayerSide; pub use crate::board::LayerType; pub use crate::board::interactors::{ - DragSelectionInteractor, InteractiveInput, SelectionCombineMode, SelectionContainMode, - SelectionOptions, + DragSelectionInteractor, DragSelectionOptions, InteractiveInput, SelectionCombineMode, + SelectionContainMode, SelectionInteractor, }; pub use crate::board::selections; pub use crate::layout::LayerId;