From e9ad380a58c34dc239e79c6be4677f645c4a850d Mon Sep 17 00:00:00 2001 From: Alain Emilia Anna Zscheile Date: Thu, 2 Jan 2025 20:38:25 +0100 Subject: [PATCH] feat(egui/overlay/bbox): allow multiple selection kinds of drag-selected BBoxes Toggling is triggered by holding down `Ctrl` during `drag_start`. Addition is triggered by holding down `Shift` during `drag_start`. Substitution is used otherwise/by default. --- crates/topola-egui/src/overlay.rs | 79 ++++++++++++++++++------------ crates/topola-egui/src/viewport.rs | 9 +++- src/autorouter/selection.rs | 24 +++++++++ 3 files changed, 80 insertions(+), 32 deletions(-) diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index 7aba370..98d4184 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -22,10 +22,17 @@ use topola::{ }, }; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SelectionMode { + Addition, + Substitution, + Toggling, +} + pub struct Overlay { ratsnest: Ratsnest, selection: Selection, - reselect_bbox: Option, + reselect_bbox: Option<(SelectionMode, Point)>, active_layer: usize, } @@ -54,29 +61,52 @@ impl Overlay { self.reselect_bbox = None; } - pub fn drag_start(&mut self, board: &Board, at: Point) { + pub fn drag_start( + &mut self, + board: &Board, + at: Point, + modifiers: &egui::Modifiers, + ) { if self.reselect_bbox.is_none() { // handle bounding box selection - self.reselect_bbox = Some(at); + let selmode = if modifiers.ctrl { + SelectionMode::Toggling + } else if modifiers.shift { + SelectionMode::Addition + } else { + SelectionMode::Substitution + }; + self.reselect_bbox = Some((selmode, at)); } } pub fn drag_stop(&mut self, board: &Board, at: Point) { - if let Some(aabb) = self.get_bbox_reselect(at) { + if let Some((selmode, aabb)) = self.get_bbox_reselect(at) { // handle bounding box selection - self.select_all_in_bbox(board, &aabb); self.reselect_bbox = None; + + match selmode { + SelectionMode::Substitution => { + self.selection = Selection::new(); + self.select_all_in_bbox(board, &aabb); + } + SelectionMode::Addition => { + self.select_all_in_bbox(board, &aabb); + } + SelectionMode::Toggling => { + let old_selection = self.take_selection(); + self.select_all_in_bbox(board, &aabb); + self.selection ^= &old_selection; + } + } } } pub fn click(&mut self, board: &Board, at: Point) { - if let Some(pt) = self.reselect_bbox.take() { + if self.reselect_bbox.is_some() { // handle bounding box selection (takes precendence over other interactions) // this is mostly in order to allow the user to recover from a missed/dropped drag_stop event - self.select_all_in_bbox( - board, - &AABB::from_corners([pt.x(), pt.y()], [at.x(), at.y()]), - ); + self.drag_stop(board, at); return; } @@ -105,23 +135,8 @@ impl Overlay { board: &Board, aabb: &AABB<[f64; 2]>, ) { - use rstar::Envelope; - for &geom in board - .layout() - .drawing() - .rtree() - .locate_in_envelope_intersecting(&AABB::<[f64; 3]>::from_corners( - [aabb.lower()[0], aabb.lower()[1], -f64::INFINITY], - [aabb.upper()[0], aabb.upper()[1], f64::INFINITY], - )) - { - let node = geom.data; - if aabb.contains_envelope(&board.layout().node_bbox(node)) - && board.layout().is_node_in_layer(node, self.active_layer) - { - self.selection.select_at_node(board, node); - } - } + self.selection + .select_all_in_bbox(board, aabb, self.active_layer); } pub fn ratsnest(&self) -> &Ratsnest { @@ -133,8 +148,12 @@ impl Overlay { } /// Returns the currently selected bounding box of a bounding-box reselect - pub fn get_bbox_reselect(&self, at: Point) -> Option> { - self.reselect_bbox - .map(|pt| AABB::from_corners([pt.x(), pt.y()], [at.x(), at.y()])) + pub fn get_bbox_reselect(&self, at: Point) -> Option<(SelectionMode, AABB<[f64; 2]>)> { + self.reselect_bbox.map(|(selmode, pt)| { + ( + selmode, + AABB::from_corners([pt.x(), pt.y()], [at.x(), at.y()]), + ) + }) } } diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index d7c15fa..18f78ee 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -93,10 +93,15 @@ impl Viewport { overlay.click(board, latest_point); } } else if response.drag_started_by(egui::PointerButton::Primary) { - overlay.drag_start(board, latest_point); + overlay.drag_start( + board, + latest_point, + &response.ctx.input(|i| i.modifiers), + ); } else if response.drag_stopped_by(egui::PointerButton::Primary) { overlay.drag_stop(board, latest_point); - } else if let Some(cur_bbox) = overlay.get_bbox_reselect(latest_point) { + } else if let Some((_, cur_bbox)) = overlay.get_bbox_reselect(latest_point) + { painter.paint_bbox_with_color(cur_bbox, egui::Color32::RED); } diff --git a/src/autorouter/selection.rs b/src/autorouter/selection.rs index cb4a881..f378bf0 100644 --- a/src/autorouter/selection.rs +++ b/src/autorouter/selection.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; +use rstar::AABB; use serde::{Deserialize, Serialize}; use crate::{ @@ -145,6 +146,29 @@ impl Selection { Self::default() } + pub fn select_all_in_bbox( + &mut self, + board: &Board, + aabb: &AABB<[f64; 2]>, + active_layer: usize, + ) { + use rstar::Envelope; + let layout = board.layout(); + for &geom in layout.drawing().rtree().locate_in_envelope_intersecting( + &AABB::<[f64; 3]>::from_corners( + [aabb.lower()[0], aabb.lower()[1], -f64::INFINITY], + [aabb.upper()[0], aabb.upper()[1], f64::INFINITY], + ), + ) { + let node = geom.data; + if aabb.contains_envelope(&layout.node_bbox(node)) + && layout.is_node_in_layer(node, active_layer) + { + self.select_at_node(board, node); + } + } + } + pub fn select_at_node(&mut self, board: &Board, node: NodeIndex) { if let Some(selector) = PinSelector::try_from_node(board, node) { self.pin_selection.0.insert(selector);