feat: allow toggling of all nodes in selected bbox

The complication here is that we need to take extra care to avoid
issuing self-vanishing/repeated togglings of the same pin/band,
as multiple graph nodes correspond to the same one (at least in case of polygons)

- refactor(layout): move reusable functions from Overlay to Layout
This commit is contained in:
Alain Emilia Anna Zscheile 2024-12-31 23:33:04 +01:00
parent b070cd787b
commit eaadc60265
10 changed files with 149 additions and 64 deletions

View File

@ -49,9 +49,12 @@ petgraph.workspace = true
rstar.workspace = true
serde.workspace = true
spade.workspace = true
specctra-core.path = "crates/specctra-core"
thiserror.workspace = true
[dependencies.specctra-core]
path = "crates/specctra-core"
features = ["rstar"]
[dev-dependencies]
serde_json.workspace = true

View File

@ -14,3 +14,7 @@ serde.workspace = true
specctra_derive.path = "../specctra_derive"
thiserror.workspace = true
utf8-chars = "3.0"
[dependencies.rstar]
workspace = true
optional = true

View File

@ -29,6 +29,16 @@ impl Circle {
y: self.pos.0.y + self.r * phi.sin()
}
}
/// The (x,y) axis aligned bounding box for this circle.
#[cfg(feature = "rstar")]
pub fn bbox(&self, margin: f64) -> rstar::AABB<[f64; 2]> {
let r = self.r + margin;
rstar::AABB::from_corners(
[self.pos.0.x - r, self.pos.0.y - r],
[self.pos.0.x + r, self.pos.0.y + r],
)
}
}
impl Sub for Circle {

View File

@ -57,7 +57,7 @@ impl Trigger {
pub fn consume_key_triggered(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> bool {
self.consume_key(ctx, ui);
self.triggered()
self.triggered
}
fn consume_key(&mut self, ctx: &egui::Context, _ui: &mut egui::Ui) {

View File

@ -78,9 +78,10 @@ pub struct EditActions {
pub undo: Trigger,
pub redo: Trigger,
pub abort: Trigger,
pub remove_bands: Trigger,
pub reset_bbox: Trigger,
pub reselect_bbox: Trigger,
pub toggle_all_in_bbox: Trigger,
pub remove_bands: Trigger,
}
impl EditActions {
@ -104,12 +105,6 @@ impl EditActions {
egui::Key::Escape,
)
.into_trigger(),
remove_bands: Action::new(
tr.text("tr-menu-edit-remove-bands"),
egui::Modifiers::NONE,
egui::Key::Delete,
)
.into_trigger(),
reset_bbox: Action::new(
tr.text("tr-menu-edit-reset-bbox"),
egui::Modifiers::CTRL,
@ -122,6 +117,18 @@ impl EditActions {
egui::Key::B,
)
.into_trigger(),
toggle_all_in_bbox: Action::new(
tr.text("tr-menu-edit-toggle-all-in-bbox"),
egui::Modifiers::NONE,
egui::Key::A,
)
.into_trigger(),
remove_bands: Action::new(
tr.text("tr-menu-edit-remove-bands"),
egui::Modifiers::NONE,
egui::Key::Delete,
)
.into_trigger(),
}
}
@ -144,6 +151,7 @@ impl EditActions {
self.reset_bbox.button(ctx, ui);
self.reselect_bbox.button(ctx, ui);
self.toggle_all_in_bbox.button(ctx, ui);
//ui.add_enabled_ui(workspace_activities_enabled, |ui| {
self.remove_bands.button(ctx, ui);

View File

@ -236,6 +236,13 @@ impl MenuBar {
workspace.overlay.reset_selected_bbox();
} else if actions.edit.reselect_bbox.consume_key_triggered(ctx, ui) {
workspace.overlay.start_bbox_reselect();
} else if actions
.edit
.toggle_all_in_bbox
.consume_key_triggered(ctx, ui)
{
let board = workspace.interactor.invoker().autorouter().board();
workspace.overlay.toggle_all_in_selected_bbox(board);
} else if actions.place.place_via.consume_key_enabled(
ctx,
ui,
@ -283,7 +290,7 @@ impl MenuBar {
pub fn update_view_menu(
&mut self,
ctx: &egui::Context,
_ctx: &egui::Context,
ui: &mut egui::Ui,
tr: &Translator,
viewport: &mut Viewport,

View File

@ -18,7 +18,7 @@ use topola::{
layout::{
poly::{MakePolyShape, PolyWeight},
via::ViaWeight,
CompoundWeight, NodeIndex,
CompoundWeight, Layout, NodeIndex,
},
};
@ -84,61 +84,36 @@ impl Overlay {
.collect();
if let Some(geom) = geoms.iter().find(|&&geom| {
self.contains_point(board, geom.data, at)
&& match geom.data {
NodeIndex::Primitive(primitive) => {
primitive.primitive(board.layout().drawing()).layer() == self.active_layer
}
NodeIndex::Compound(compound) => {
match board.layout().drawing().compound_weight(compound) {
CompoundWeight::Poly(_) => {
board
.layout()
.poly(GenericIndex::<PolyWeight>::new(
compound.petgraph_index(),
))
.layer()
== self.active_layer
}
CompoundWeight::Via(weight) => {
weight.from_layer >= self.active_layer
&& weight.to_layer <= self.active_layer
}
}
}
}
board.layout().node_shape(geom.data).contains_point(at)
&& board
.layout()
.is_node_in_layer(geom.data, self.active_layer)
}) {
self.selection.toggle_at_node(board, geom.data);
}
}
fn contains_point(
&self,
board: &Board<impl AccessMesadata>,
node: NodeIndex,
p: Point,
) -> bool {
let shape: Shape = match node {
NodeIndex::Primitive(primitive) => {
primitive.primitive(board.layout().drawing()).shape().into()
}
NodeIndex::Compound(compound) => {
match board.layout().drawing().compound_weight(compound) {
CompoundWeight::Poly(_) => board
.layout()
.poly(GenericIndex::<PolyWeight>::new(compound.petgraph_index()))
.shape()
.into(),
CompoundWeight::Via(_) => board
.layout()
.via(GenericIndex::<ViaWeight>::new(compound.petgraph_index()))
.shape()
.into(),
}
}
};
shape.contains_point(p)
pub fn toggle_all_in_selected_bbox(&mut self, board: &Board<impl AccessMesadata>) {
use rstar::Envelope;
let aabb = self.selected_bbox;
let mut temp_selection = Selection::new();
for node 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],
))
.map(|&geom| geom.data)
.filter(|&node| {
aabb.contains_envelope(&board.layout().node_bbox(node))
&& board.layout().is_node_in_layer(node, self.active_layer)
})
{
temp_selection.select_at_node(board, node);
}
self.selection ^= &temp_selection;
}
pub fn ratsnest(&self) -> &Ratsnest {

View File

@ -14,6 +14,7 @@ tr-menu-edit-redo = Redo
tr-menu-edit-abort = Abort
tr-menu-edit-reset-bbox = Reset selected BBox
tr-menu-edit-reselect-bbox = (Re-)select BBox
tr-menu-edit-toggle-all-in-bbox = Toggle all entries in selected BBox
tr-menu-edit-remove-bands = Remove Bands
tr-menu-view = View

View File

@ -145,6 +145,14 @@ impl Selection {
Self::default()
}
pub fn select_at_node(&mut self, board: &Board<impl AccessMesadata>, node: NodeIndex) {
if let Some(selector) = PinSelector::try_from_node(board, node) {
self.pin_selection.0.insert(selector);
} else if let Some(selector) = BandSelector::try_from_node(board, node) {
self.band_selection.0.insert(selector);
}
}
pub fn toggle_at_node(&mut self, board: &Board<impl AccessMesadata>, node: NodeIndex) {
if let Some(selector) = PinSelector::try_from_node(board, node) {
if self.pin_selection.0.contains(&selector) {
@ -166,3 +174,10 @@ impl Selection {
|| self.band_selection.contains_node(board, node)
}
}
impl<'a> core::ops::BitXorAssign<&'a Selection> for Selection {
fn bitxor_assign(&mut self, rhs: &'a Selection) {
self.pin_selection.0 = &self.pin_selection.0 ^ &rhs.pin_selection.0;
self.band_selection.0 = &self.band_selection.0 ^ &rhs.band_selection.0;
}
}

View File

@ -15,7 +15,8 @@ use crate::{
cane::Cane,
dot::{DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, LooseDotIndex, LooseDotWeight},
gear::GearIndex,
graph::{GetMaybeNet, PrimitiveIndex, PrimitiveWeight},
graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex, PrimitiveWeight},
primitive::MakePrimitiveShape,
rules::AccessRules,
seg::{
FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex,
@ -23,10 +24,10 @@ use crate::{
},
Drawing, DrawingEdit, DrawingException, Infringement,
},
geometry::{edit::ApplyGeometryEdit, GenericNode},
geometry::{edit::ApplyGeometryEdit, shape::Shape, GenericNode},
graph::{GenericIndex, GetPetgraphIndex},
layout::{
poly::{Poly, PolyWeight},
poly::{MakePolyShape, Poly, PolyWeight},
via::{Via, ViaWeight},
},
};
@ -317,6 +318,67 @@ impl<R: AccessRules> Layout<R> {
.compound_members(GenericIndex::new(poly.petgraph_index()))
}
pub fn is_node_in_layer(&self, index: NodeIndex, active_layer: usize) -> bool {
use crate::drawing::graph::GetLayer;
match index {
NodeIndex::Primitive(primitive) => {
primitive.primitive(&self.drawing).layer() == active_layer
}
NodeIndex::Compound(compound) => match self.drawing.compound_weight(compound) {
CompoundWeight::Poly(_) => {
self.poly(GenericIndex::<PolyWeight>::new(compound.petgraph_index()))
.layer()
== active_layer
}
CompoundWeight::Via(weight) => {
weight.from_layer >= active_layer && weight.to_layer <= active_layer
}
},
}
}
pub fn node_shape(&self, index: NodeIndex) -> Shape {
match index {
NodeIndex::Primitive(primitive) => primitive.primitive(&self.drawing).shape().into(),
NodeIndex::Compound(compound) => match self.drawing.compound_weight(compound) {
CompoundWeight::Poly(_) => self
.poly(GenericIndex::<PolyWeight>::new(compound.petgraph_index()))
.shape()
.into(),
CompoundWeight::Via(_) => self
.via(GenericIndex::<ViaWeight>::new(compound.petgraph_index()))
.shape()
.into(),
},
}
}
pub fn node_bbox(&self, index: NodeIndex) -> AABB<[f64; 2]> {
use crate::geometry::primitive::AccessPrimitiveShape;
match index {
NodeIndex::Primitive(primitive) => primitive.primitive(&self.drawing).shape().bbox(0.0),
NodeIndex::Compound(compound) => match self.drawing.compound_weight(compound) {
CompoundWeight::Poly(_) => {
let coord_string = self
.poly(GenericIndex::<PolyWeight>::new(compound.petgraph_index()))
.shape()
.polygon
.exterior()
.0
.iter()
.map(|coord| [coord.x, coord.y])
.collect::<Vec<_>>();
AABB::from_points(&coord_string[..])
}
CompoundWeight::Via(_) => self
.via(GenericIndex::<ViaWeight>::new(compound.petgraph_index()))
.shape()
.bbox(0.0),
},
}
}
pub fn rules(&self) -> &R {
self.drawing.rules()
}