diff --git a/Cargo.toml b/Cargo.toml index 68528e7..d681f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/specctra-core/Cargo.toml b/crates/specctra-core/Cargo.toml index 1673264..c1462a9 100644 --- a/crates/specctra-core/Cargo.toml +++ b/crates/specctra-core/Cargo.toml @@ -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 diff --git a/crates/specctra-core/src/math.rs b/crates/specctra-core/src/math.rs index 88a1d20..4911e2f 100644 --- a/crates/specctra-core/src/math.rs +++ b/crates/specctra-core/src/math.rs @@ -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 { diff --git a/crates/topola-egui/src/action.rs b/crates/topola-egui/src/action.rs index df8a10b..6effe39 100644 --- a/crates/topola-egui/src/action.rs +++ b/crates/topola-egui/src/action.rs @@ -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) { diff --git a/crates/topola-egui/src/actions.rs b/crates/topola-egui/src/actions.rs index 83f0fd7..7402b21 100644 --- a/crates/topola-egui/src/actions.rs +++ b/crates/topola-egui/src/actions.rs @@ -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); diff --git a/crates/topola-egui/src/menu_bar.rs b/crates/topola-egui/src/menu_bar.rs index 58c90a3..62764c0 100644 --- a/crates/topola-egui/src/menu_bar.rs +++ b/crates/topola-egui/src/menu_bar.rs @@ -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, diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index 8546755..51eea85 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -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::::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, - 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::::new(compound.petgraph_index())) - .shape() - .into(), - CompoundWeight::Via(_) => board - .layout() - .via(GenericIndex::::new(compound.petgraph_index())) - .shape() - .into(), - } - } - }; - - shape.contains_point(p) + pub fn toggle_all_in_selected_bbox(&mut self, board: &Board) { + 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 { diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 8451d45..eb490da 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -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 diff --git a/src/autorouter/selection.rs b/src/autorouter/selection.rs index 3a864df..cb4a881 100644 --- a/src/autorouter/selection.rs +++ b/src/autorouter/selection.rs @@ -145,6 +145,14 @@ impl Selection { Self::default() } + 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); + } 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, 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; + } +} diff --git a/src/layout/layout.rs b/src/layout/layout.rs index 1d68286..8279f03 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -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 Layout { .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::::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::::new(compound.petgraph_index())) + .shape() + .into(), + CompoundWeight::Via(_) => self + .via(GenericIndex::::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::::new(compound.petgraph_index())) + .shape() + .polygon + .exterior() + .0 + .iter() + .map(|coord| [coord.x, coord.y]) + .collect::>(); + + AABB::from_points(&coord_string[..]) + } + CompoundWeight::Via(_) => self + .via(GenericIndex::::new(compound.petgraph_index())) + .shape() + .bbox(0.0), + }, + } + } + pub fn rules(&self) -> &R { self.drawing.rules() }