feat(selection): BboxSelectionKind (completely inside vs. merely intersects)

This commit is contained in:
Alain Emilia Anna Zscheile 2025-01-03 16:23:42 +01:00 committed by mikolaj
parent 4c2f9e3984
commit 740019e2e5
5 changed files with 222 additions and 28 deletions

View File

@ -7,7 +7,10 @@ use rstar::{Point as _, AABB};
use spade::InsertionError; use spade::InsertionError;
use topola::{ use topola::{
autorouter::{ratsnest::Ratsnest, selection::Selection}, autorouter::{
ratsnest::Ratsnest,
selection::{BboxSelectionKind, Selection},
},
board::{mesadata::AccessMesadata, Board}, board::{mesadata::AccessMesadata, Board},
drawing::{ drawing::{
graph::{GetLayer, MakePrimitive}, graph::{GetLayer, MakePrimitive},
@ -53,7 +56,11 @@ impl Overlay {
} }
pub fn select_all(&mut self, board: &Board<impl AccessMesadata>) { pub fn select_all(&mut self, board: &Board<impl AccessMesadata>) {
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) { pub fn unselect_all(&mut self) {
@ -81,21 +88,21 @@ impl Overlay {
} }
pub fn drag_stop(&mut self, board: &Board<impl AccessMesadata>, at: Point) { pub fn drag_stop(&mut self, board: &Board<impl AccessMesadata>, 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 // handle bounding box selection
self.reselect_bbox = None; self.reselect_bbox = None;
match selmode { match selmode {
SelectionMode::Substitution => { SelectionMode::Substitution => {
self.selection = Selection::new(); self.selection = Selection::new();
self.select_all_in_bbox(board, &aabb); self.select_all_in_bbox(board, &aabb, bsk);
} }
SelectionMode::Addition => { SelectionMode::Addition => {
self.select_all_in_bbox(board, &aabb); self.select_all_in_bbox(board, &aabb, bsk);
} }
SelectionMode::Toggling => { SelectionMode::Toggling => {
let old_selection = self.take_selection(); 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; self.selection ^= &old_selection;
} }
} }
@ -134,9 +141,10 @@ impl Overlay {
&mut self, &mut self,
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
aabb: &AABB<[f64; 2]>, aabb: &AABB<[f64; 2]>,
bsk: BboxSelectionKind,
) { ) {
self.selection 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 { pub fn ratsnest(&self) -> &Ratsnest {
@ -148,10 +156,21 @@ impl Overlay {
} }
/// Returns the currently selected bounding box of a bounding-box reselect /// 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)| { self.reselect_bbox.map(|(selmode, pt)| {
( (
selmode, 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()]), AABB::from_corners([pt.x(), pt.y()], [at.x(), at.y()]),
) )
}) })

View File

@ -100,9 +100,17 @@ impl Viewport {
); );
} else if response.drag_stopped_by(egui::PointerButton::Primary) { } else if response.drag_stopped_by(egui::PointerButton::Primary) {
overlay.drag_stop(board, latest_point); 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(); let board = workspace.interactor.invoker().autorouter().board();

View File

@ -2,13 +2,13 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::collections::BTreeSet; use std::collections::{BTreeMap, BTreeSet};
use rstar::AABB; use rstar::AABB;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
board::{mesadata::AccessMesadata, BandName, Board}, board::{mesadata::AccessMesadata, BandName, Board, ResolvedSelector},
drawing::graph::{GetLayer, MakePrimitive, PrimitiveIndex}, drawing::graph::{GetLayer, MakePrimitive, PrimitiveIndex},
geometry::GenericNode, geometry::GenericNode,
graph::{GenericIndex, GetPetgraphIndex}, graph::{GenericIndex, GetPetgraphIndex},
@ -55,6 +55,17 @@ impl PinSelector {
None None
} }
} }
pub fn try_from_pin_and_layer_id(
board: &Board<impl AccessMesadata>,
pin: &str,
layer: usize,
) -> Option<PinSelector> {
Some(PinSelector {
pin: pin.to_string(),
layer: board.layout().rules().layer_layername(layer)?.to_string(),
})
}
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
@ -109,10 +120,18 @@ impl BandSelector {
_ => return None, _ => return None,
}; };
Self::try_from_uid(
board,
&board.layout().drawing().collect().loose_band_uid(loose),
)
}
pub fn try_from_uid(
board: &Board<impl AccessMesadata>,
uid: &crate::drawing::band::BandUid,
) -> Option<BandSelector> {
Some(BandSelector { Some(BandSelector {
band: board band: board.band_bandname(uid)?.clone(),
.band_bandname(&board.layout().drawing().collect().loose_band_uid(loose))?
.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)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Selection { pub struct Selection {
pub pin_selection: PinSelection, pub pin_selection: PinSelection,
@ -151,20 +186,80 @@ impl Selection {
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
aabb: &AABB<[f64; 2]>, aabb: &AABB<[f64; 2]>,
active_layer: usize, active_layer: usize,
kind: BboxSelectionKind,
) { ) {
use rstar::Envelope; const INF: f64 = f64::INFINITY;
let layout = board.layout(); let layout = board.layout();
for &geom in layout.drawing().rtree().locate_in_envelope_intersecting(
&AABB::<[f64; 3]>::from_corners( let resolved_selectors =
[aabb.lower()[0], aabb.lower()[1], -f64::INFINITY], match kind {
[aabb.upper()[0], aabb.upper()[1], f64::INFINITY], BboxSelectionKind::CompletelyInside => {
), // 1. gather relevant node indices, and group them by resolved selectors
) { // .0 collects all nodes per resolved selection
let node = geom.data; // .1 collects only nodes which are actively selected here
if aabb.contains_envelope(&layout.node_bbox(node)) let mut selectors = BTreeMap::<
&& layout.is_node_in_layer(node, active_layer) ResolvedSelector,
{ (BTreeSet<NodeIndex>, BTreeSet<NodeIndex>),
self.select_at_node(board, node); >::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::<BTreeSet<_>>()
}
BboxSelectionKind::MerelyIntersects => {
// 1. gather relevant resolved selectors
let mut selectors = BTreeSet::<ResolvedSelector>::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);
}
}
} }
} }
} }

View File

@ -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<impl AccessMesadata>, node: NodeIndex) -> Option<Self> {
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::<PolyWeight>::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. /// Represents a board layout and its associated metadata.
/// ///
/// The struct manages the relationships between board's layout, /// The struct manages the relationships between board's layout,

View File

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use core::{cmp, hash};
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use petgraph::stable_graph::NodeIndex; use petgraph::stable_graph::NodeIndex;
@ -19,7 +20,7 @@ use super::{
Drawing, Drawing,
}; };
#[derive(Clone, Copy, Debug, Ord, PartialOrd)] #[derive(Clone, Copy, Debug)]
pub struct BandUid(pub BandTermsegIndex, pub BandTermsegIndex); pub struct BandUid(pub BandTermsegIndex, pub BandTermsegIndex);
impl BandUid { impl BandUid {
@ -41,6 +42,30 @@ impl PartialEq for BandUid {
impl Eq for BandUid {} impl Eq for BandUid {}
impl hash::Hash for BandUid {
fn hash<H: hash::Hasher>(&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<cmp::Ordering> {
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)] #[enum_dispatch(GetPetgraphIndex)]
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BandTermsegIndex { pub enum BandTermsegIndex {