diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index 98d4184..8b687ec 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -7,7 +7,10 @@ use rstar::{Point as _, AABB}; use spade::InsertionError; use topola::{ - autorouter::{ratsnest::Ratsnest, selection::Selection}, + autorouter::{ + ratsnest::Ratsnest, + selection::{BboxSelectionKind, Selection}, + }, board::{mesadata::AccessMesadata, Board}, drawing::{ graph::{GetLayer, MakePrimitive}, @@ -53,7 +56,11 @@ impl Overlay { } pub fn select_all(&mut self, board: &Board) { - self.select_all_in_bbox(board, &AABB::from_corners([-INF, -INF], [INF, INF])); + self.select_all_in_bbox( + board, + &AABB::from_corners([-INF, -INF], [INF, INF]), + BboxSelectionKind::CompletelyInside, + ); } pub fn unselect_all(&mut self) { @@ -81,21 +88,21 @@ impl Overlay { } pub fn drag_stop(&mut self, board: &Board, at: Point) { - if let Some((selmode, aabb)) = self.get_bbox_reselect(at) { + if let Some((selmode, bsk, aabb)) = self.get_bbox_reselect(at) { // handle bounding box selection self.reselect_bbox = None; match selmode { SelectionMode::Substitution => { self.selection = Selection::new(); - self.select_all_in_bbox(board, &aabb); + self.select_all_in_bbox(board, &aabb, bsk); } SelectionMode::Addition => { - self.select_all_in_bbox(board, &aabb); + self.select_all_in_bbox(board, &aabb, bsk); } SelectionMode::Toggling => { let old_selection = self.take_selection(); - self.select_all_in_bbox(board, &aabb); + self.select_all_in_bbox(board, &aabb, bsk); self.selection ^= &old_selection; } } @@ -134,9 +141,10 @@ impl Overlay { &mut self, board: &Board, aabb: &AABB<[f64; 2]>, + bsk: BboxSelectionKind, ) { self.selection - .select_all_in_bbox(board, aabb, self.active_layer); + .select_all_in_bbox(board, aabb, self.active_layer, bsk); } pub fn ratsnest(&self) -> &Ratsnest { @@ -148,10 +156,21 @@ impl Overlay { } /// Returns the currently selected bounding box of a bounding-box reselect - pub fn get_bbox_reselect(&self, at: Point) -> Option<(SelectionMode, AABB<[f64; 2]>)> { + pub fn get_bbox_reselect( + &self, + at: Point, + ) -> Option<(SelectionMode, BboxSelectionKind, AABB<[f64; 2]>)> { self.reselect_bbox.map(|(selmode, pt)| { ( selmode, + // Δx = at.x() - pt.x() + if at.x() <= pt.x() { + // Δx ≤ 0 + BboxSelectionKind::MerelyIntersects + } else { + // Δx > 0 + BboxSelectionKind::CompletelyInside + }, 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 18f78ee..d931604 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -100,9 +100,17 @@ impl Viewport { ); } 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((_, bsk, cur_bbox)) = + overlay.get_bbox_reselect(latest_point) { - painter.paint_bbox_with_color(cur_bbox, egui::Color32::RED); + use topola::autorouter::selection::BboxSelectionKind; + painter.paint_bbox_with_color( + cur_bbox, + match bsk { + BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW, + BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE, + }, + ); } let board = workspace.interactor.invoker().autorouter().board(); diff --git a/src/autorouter/selection.rs b/src/autorouter/selection.rs index 8b8fb01..90cfe8e 100644 --- a/src/autorouter/selection.rs +++ b/src/autorouter/selection.rs @@ -2,13 +2,13 @@ // // SPDX-License-Identifier: MIT -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use rstar::AABB; use serde::{Deserialize, Serialize}; use crate::{ - board::{mesadata::AccessMesadata, BandName, Board}, + board::{mesadata::AccessMesadata, BandName, Board, ResolvedSelector}, drawing::graph::{GetLayer, MakePrimitive, PrimitiveIndex}, geometry::GenericNode, graph::{GenericIndex, GetPetgraphIndex}, @@ -55,6 +55,17 @@ impl PinSelector { None } } + + pub fn try_from_pin_and_layer_id( + board: &Board, + pin: &str, + layer: usize, + ) -> Option { + Some(PinSelector { + pin: pin.to_string(), + layer: board.layout().rules().layer_layername(layer)?.to_string(), + }) + } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -109,10 +120,18 @@ impl BandSelector { _ => return None, }; + Self::try_from_uid( + board, + &board.layout().drawing().collect().loose_band_uid(loose), + ) + } + + pub fn try_from_uid( + board: &Board, + uid: &crate::drawing::band::BandUid, + ) -> Option { Some(BandSelector { - band: board - .band_bandname(&board.layout().drawing().collect().loose_band_uid(loose))? - .clone(), + band: board.band_bandname(uid)?.clone(), }) } } @@ -135,6 +154,22 @@ impl BandSelection { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BboxSelectionKind { + CompletelyInside, + MerelyIntersects, +} + +impl BboxSelectionKind { + pub fn matches(&self, bigger: &AABB<[f64; 2]>, smaller: &AABB<[f64; 2]>) -> bool { + use rstar::Envelope; + match self { + Self::CompletelyInside => bigger.contains_envelope(&smaller), + Self::MerelyIntersects => bigger.intersection_area(&smaller) > 0.0, + } + } +} + #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Selection { pub pin_selection: PinSelection, @@ -151,20 +186,80 @@ impl Selection { board: &Board, aabb: &AABB<[f64; 2]>, active_layer: usize, + kind: BboxSelectionKind, ) { - use rstar::Envelope; + const INF: f64 = f64::INFINITY; 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); + + let resolved_selectors = + match kind { + BboxSelectionKind::CompletelyInside => { + // 1. gather relevant node indices, and group them by resolved selectors + // .0 collects all nodes per resolved selection + // .1 collects only nodes which are actively selected here + let mut selectors = BTreeMap::< + ResolvedSelector, + (BTreeSet, BTreeSet), + >::new(); + for &geom in layout.drawing().rtree().locate_in_envelope_intersecting( + &AABB::<[f64; 3]>::from_corners([-INF, -INF, -INF], [INF, INF, INF]), + ) { + let node = geom.data; + if layout.is_node_in_layer(node, active_layer) { + if let Some(rsel) = ResolvedSelector::try_from_node(board, node) { + let rseli = selectors.entry(rsel).or_default(); + rseli.0.insert(node); + if kind.matches(aabb, &layout.node_bbox(node)) { + rseli.1.insert(node); + } + } + } + } + + // 2. restrict to complete matches, return associated keys + selectors + .into_iter() + .filter(|(_, nis)| &nis.0 == &nis.1) + .map(|(k, _)| k) + .collect::>() + } + BboxSelectionKind::MerelyIntersects => { + // 1. gather relevant resolved selectors + let mut selectors = BTreeSet::::new(); + 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 layout.is_node_in_layer(node, active_layer) + && kind.matches(aabb, &layout.node_bbox(node)) + { + if let Some(rsel) = ResolvedSelector::try_from_node(board, node) { + selectors.insert(rsel); + } + } + } + // 2. nothing to restrict + selectors + } + }; + + // 3. convert resolved selectors to actual selections + for i in resolved_selectors { + match i { + ResolvedSelector::Band { band_uid } => { + if let Some(x) = BandSelector::try_from_uid(board, &band_uid) { + self.band_selection.0.insert(x); + } + } + ResolvedSelector::Pin { pin_name, layer } => { + if let Some(x) = PinSelector::try_from_pin_and_layer_id(board, pin_name, layer) + { + self.pin_selection.0.insert(x); + } + } } } } diff --git a/src/board/mod.rs b/src/board/mod.rs index d1f0fe9..eb00fea 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -51,6 +51,53 @@ impl BandName { } } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ResolvedSelector<'a> { + Band { band_uid: BandUid }, + Pin { pin_name: &'a str, layer: usize }, +} + +impl<'a> ResolvedSelector<'a> { + pub fn try_from_node(board: &'a Board, node: NodeIndex) -> Option { + use crate::{drawing::graph::MakePrimitive, graph::GetPetgraphIndex}; + + let (layer, loose) = match node { + NodeIndex::Primitive(primitive) => ( + primitive.primitive(board.layout().drawing()).layer(), + match primitive { + PrimitiveIndex::LooseDot(dot) => Some(dot.into()), + PrimitiveIndex::LoneLooseSeg(seg) => Some(seg.into()), + PrimitiveIndex::SeqLooseSeg(seg) => Some(seg.into()), + PrimitiveIndex::LooseBend(bend) => Some(bend.into()), + _ => None, + }, + ), + NodeIndex::Compound(compound) => { + match board.layout().drawing().compound_weight(compound) { + CompoundWeight::Poly(..) => ( + board + .layout() + .poly(GenericIndex::::new(compound.petgraph_index())) + .layer(), + None, + ), + _ => return None, + } + } + }; + + if let Some(pin_name) = board.node_pinname(&node) { + Some(ResolvedSelector::Pin { pin_name, layer }) + } else if let Some(loose) = loose { + Some(ResolvedSelector::Band { + band_uid: board.layout().drawing().collect().loose_band_uid(loose), + }) + } else { + None + } + } +} + /// Represents a board layout and its associated metadata. /// /// The struct manages the relationships between board's layout, diff --git a/src/drawing/band.rs b/src/drawing/band.rs index 6e3af7e..2bcd89b 100644 --- a/src/drawing/band.rs +++ b/src/drawing/band.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: MIT +use core::{cmp, hash}; use enum_dispatch::enum_dispatch; use petgraph::stable_graph::NodeIndex; @@ -19,7 +20,7 @@ use super::{ Drawing, }; -#[derive(Clone, Copy, Debug, Ord, PartialOrd)] +#[derive(Clone, Copy, Debug)] pub struct BandUid(pub BandTermsegIndex, pub BandTermsegIndex); impl BandUid { @@ -41,6 +42,30 @@ impl PartialEq for BandUid { impl Eq for BandUid {} +impl hash::Hash for BandUid { + fn hash(&self, state: &mut H) { + self.0.petgraph_index().hash(state); + self.1.petgraph_index().hash(state); + } +} + +impl cmp::PartialOrd for BandUid { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl cmp::Ord for BandUid { + fn cmp(&self, other: &Self) -> cmp::Ordering { + use cmp::Ordering as O; + match self.0.petgraph_index().cmp(&other.0.petgraph_index()) { + O::Less => O::Less, + O::Greater => O::Greater, + O::Equal => self.1.petgraph_index().cmp(&other.1.petgraph_index()), + } + } +} + #[enum_dispatch(GetPetgraphIndex)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum BandTermsegIndex {