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 7e875c8..c9dfbdc 100644 --- a/crates/topola-egui/src/actions.rs +++ b/crates/topola-egui/src/actions.rs @@ -78,6 +78,8 @@ pub struct EditActions { pub undo: Trigger, pub redo: Trigger, pub abort: Trigger, + pub select_all: Trigger, + pub unselect_all: Trigger, pub remove_bands: Trigger, } @@ -102,6 +104,18 @@ impl EditActions { egui::Key::Escape, ) .into_trigger(), + select_all: Action::new( + tr.text("tr-menu-edit-select-all"), + egui::Modifiers::CTRL, + egui::Key::A, // taken from KiCAD + ) + .into_trigger(), + unselect_all: Action::new( + tr.text("tr-menu-edit-unselect-all"), + egui::Modifiers::CTRL | egui::Modifiers::SHIFT, + egui::Key::A, + ) + .into_trigger(), remove_bands: Action::new( tr.text("tr-menu-edit-remove-bands"), egui::Modifiers::NONE, @@ -128,6 +142,11 @@ impl EditActions { ui.separator(); + self.select_all.button(ctx, ui); + self.unselect_all.button(ctx, ui); + + ui.separator(); + //ui.add_enabled_ui(workspace_activities_enabled, |ui| { self.remove_bands.button(ctx, ui); //}); @@ -174,7 +193,7 @@ impl RouteActions { autoroute: Action::new( tr.text("tr-menu-route-autoroute"), egui::Modifiers::CTRL, - egui::Key::A, + egui::Key::R, ) .into_trigger(), } diff --git a/crates/topola-egui/src/menu_bar.rs b/crates/topola-egui/src/menu_bar.rs index 561b7ec..1caeeed 100644 --- a/crates/topola-egui/src/menu_bar.rs +++ b/crates/topola-egui/src/menu_bar.rs @@ -232,6 +232,12 @@ impl MenuBar { workspace.interactor.redo(); } else if actions.edit.abort.consume_key_triggered(ctx, ui) { workspace.interactor.abort(); + } else if actions.edit.unselect_all.consume_key_triggered(ctx, ui) { + // NOTE: we need to check `unselect` first because `Ctrl+A` would also match `Ctrl+Shift+A` + workspace.overlay.unselect_all(); + } else if actions.edit.select_all.consume_key_triggered(ctx, ui) { + let board = workspace.interactor.invoker().autorouter().board(); + workspace.overlay.select_all(board); } else if actions.place.place_via.consume_key_enabled( ctx, ui, diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index 325df9f..98d4184 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use geo::Point; -use rstar::AABB; +use rstar::{Point as _, AABB}; use spade::InsertionError; use topola::{ @@ -18,21 +18,32 @@ use topola::{ layout::{ poly::{MakePolyShape, PolyWeight}, via::ViaWeight, - CompoundWeight, NodeIndex, + CompoundWeight, Layout, NodeIndex, }, }; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SelectionMode { + Addition, + Substitution, + Toggling, +} + pub struct Overlay { ratsnest: Ratsnest, selection: Selection, + reselect_bbox: Option<(SelectionMode, Point)>, active_layer: usize, } +const INF: f64 = f64::INFINITY; + impl Overlay { pub fn new(board: &Board) -> Result { Ok(Self { ratsnest: Ratsnest::new(board.layout())?, selection: Selection::new(), + reselect_bbox: None, active_layer: 0, }) } @@ -41,11 +52,64 @@ impl Overlay { core::mem::replace(&mut self.selection, Selection::new()) } - pub fn clear_selection(&mut self) { + pub fn select_all(&mut self, board: &Board) { + self.select_all_in_bbox(board, &AABB::from_corners([-INF, -INF], [INF, INF])); + } + + pub fn unselect_all(&mut self) { self.selection = Selection::new(); + self.reselect_bbox = None; + } + + pub fn drag_start( + &mut self, + board: &Board, + at: Point, + modifiers: &egui::Modifiers, + ) { + if self.reselect_bbox.is_none() { + // handle bounding box selection + 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((selmode, 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); + } + 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 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.drag_stop(board, at); + return; + } + let geoms: Vec<_> = board .layout() .drawing() @@ -57,61 +121,22 @@ 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, + pub fn select_all_in_bbox( + &mut 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) + aabb: &AABB<[f64; 2]>, + ) { + self.selection + .select_all_in_bbox(board, aabb, self.active_layer); } pub fn ratsnest(&self) -> &Ratsnest { @@ -121,4 +146,14 @@ impl Overlay { pub fn selection(&self) -> &Selection { &self.selection } + + /// Returns the currently selected bounding box of a bounding-box reselect + 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/painter.rs b/crates/topola-egui/src/painter.rs index 8a48ad6..65d0f05 100644 --- a/crates/topola-egui/src/painter.rs +++ b/crates/topola-egui/src/painter.rs @@ -59,6 +59,10 @@ impl<'a> Painter<'a> { } pub fn paint_bbox(&mut self, bbox: AABB<[f64; 2]>) { + self.paint_bbox_with_color(bbox, egui::Color32::GRAY) + } + + pub fn paint_bbox_with_color(&mut self, bbox: AABB<[f64; 2]>, color: egui::Color32) { let rect = egui::epaint::Rect { min: [bbox.lower()[0] as f32, -bbox.upper()[1] as f32].into(), max: [bbox.upper()[0] as f32, -bbox.lower()[1] as f32].into(), @@ -66,7 +70,7 @@ impl<'a> Painter<'a> { self.ui.painter().add(egui::Shape::rect_stroke( self.transform * rect, egui::Rounding::ZERO, - egui::Stroke::new(1.0, egui::Color32::GRAY), + egui::Stroke::new(1.0, color), )); } diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index 633fa41..18f78ee 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -45,232 +45,320 @@ impl Viewport { menu_bar: &MenuBar, maybe_workspace: Option<&mut Workspace>, ) -> egui::Rect { - egui::CentralPanel::default().show(ctx, |ui| { - egui::Frame::canvas(ui.style()).show(ui, |ui| { - ui.ctx().request_repaint(); + egui::CentralPanel::default() + .show(ctx, |ui| { + egui::Frame::canvas(ui.style()).show(ui, |ui| { + ui.ctx().request_repaint(); - let (_id, viewport_rect) = ui.allocate_space(ui.available_size()); - let latest_pos = self.transform.inverse() * (ctx.input(|i| i.pointer.latest_pos().unwrap_or_default())); + let (id, viewport_rect) = ui.allocate_space(ui.available_size()); + let response = ui.interact(viewport_rect, id, egui::Sense::click_and_drag()); + // NOTE: we use `interact_pos` instead of `latest_pos` to handle "pointer gone" + // events more graceful + let latest_pos = self.transform.inverse() + * (response.interact_pointer_pos().unwrap_or_else(|| { + ctx.input(|i| i.pointer.interact_pos().unwrap_or_default()) + })); - let old_scaling = self.transform.scaling; - self.transform.scaling *= ctx.input(|i| i.zoom_delta()); + let old_scaling = self.transform.scaling; + self.transform.scaling *= ctx.input(|i| i.zoom_delta()); - self.transform.translation += latest_pos.to_vec2() * (old_scaling - self.transform.scaling); - self.transform.translation += ctx.input(|i| i.smooth_scroll_delta); + self.transform.translation += + latest_pos.to_vec2() * (old_scaling - self.transform.scaling); + self.transform.translation += ctx.input(|i| i.smooth_scroll_delta); - let mut painter = Painter::new(ui, self.transform, menu_bar.show_bboxes); + let mut painter = Painter::new(ui, self.transform, menu_bar.show_bboxes); - if let Some(workspace) = maybe_workspace { - let layers = &mut workspace.appearance_panel; - let overlay = &mut workspace.overlay; + if let Some(workspace) = maybe_workspace { + let layers = &mut workspace.appearance_panel; + let overlay = &mut workspace.overlay; + let latest_point = point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}; + let board = workspace.interactor.invoker().autorouter().board(); - if ctx.input(|i| i.pointer.any_click()) { - if menu_bar.is_placing_via { - workspace.interactor.execute( - Command::PlaceVia(ViaWeight { + if response.clicked_by(egui::PointerButton::Primary) { + if menu_bar.is_placing_via { + workspace.interactor.execute(Command::PlaceVia(ViaWeight { from_layer: 0, to_layer: 0, circle: Circle { - pos: point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, - r: menu_bar.autorouter_options.router_options.routed_band_width / 2.0, + pos: latest_point, + r: menu_bar + .autorouter_options + .router_options + .routed_band_width + / 2.0, }, maybe_net: Some(1234), - }), - ); - } else { - overlay.click( - workspace.interactor.invoker().autorouter().board(), - point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, + })); + } else { + overlay.click(board, latest_point); + } + } else if response.drag_started_by(egui::PointerButton::Primary) { + 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) + { + painter.paint_bbox_with_color(cur_bbox, egui::Color32::RED); } - } - let board = workspace.interactor.invoker().autorouter().board(); + let board = workspace.interactor.invoker().autorouter().board(); - for i in (0..layers.visible.len()).rev() { - if layers.visible[i] { - for primitive in board.layout().drawing().layer_primitive_nodes(i) { - let shape = primitive.primitive(board.layout().drawing()).shape(); + for i in (0..layers.visible.len()).rev() { + if layers.visible[i] { + for primitive in board.layout().drawing().layer_primitive_nodes(i) { + let shape = + primitive.primitive(board.layout().drawing()).shape(); - let color = if overlay - .selection() - .contains_node(board, GenericNode::Primitive(primitive)) - { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).highlighted - } else if let Some(activity) = &mut workspace.interactor.maybe_activity() { - if activity.obstacles().contains(&primitive) { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).highlighted + let color = if overlay + .selection() + .contains_node(board, GenericNode::Primitive(primitive)) + { + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .highlighted + } else if let Some(activity) = + &mut workspace.interactor.maybe_activity() + { + if activity.obstacles().contains(&primitive) { + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .highlighted + } else { + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .normal + } } else { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).normal - } - } else { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).normal - }; - - painter.paint_primitive(&shape, color); - } - - for poly in board.layout().layer_poly_nodes(i) { - let color = if overlay - .selection() - .contains_node(board, GenericNode::Compound(poly.into())) - { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).highlighted - } else { - config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).normal - }; - - painter.paint_polygon(&board.layout().poly(poly).shape().polygon, color) - } - } - } - - if menu_bar.show_ratsnest { - let graph = overlay.ratsnest().graph(); - for edge in graph.edge_references() { - let from = graph - .node_weight(edge.source()) - .unwrap() - .pos; - let to = graph - .node_weight(edge.target()) - .unwrap() - .pos; - - painter.paint_edge( - from, - to, - egui::Stroke::new(1.0, egui::Color32::from_rgb(90, 90, 200)), - ); - } - } - - if menu_bar.show_navmesh { - if let Some(activity) = workspace.interactor.maybe_activity() { - if let Some(navmesh) = activity.maybe_navmesh() { - for edge in navmesh.edge_references() { - let mut from = PrimitiveIndex::from(navmesh.node_weight(edge.source()).unwrap().node) - .primitive(board.layout().drawing()) - .shape() - .center(); - let mut to = PrimitiveIndex::from(navmesh.node_weight(edge.target()).unwrap().node) - .primitive(board.layout().drawing()) - .shape() - .center(); - - if let Some(from_cw) = navmesh.node_weight(edge.source()).unwrap().maybe_cw { - if from_cw { - from -= [0.0, 150.0].into(); - } else { - from += [0.0, 150.0].into(); - } - } - - if let Some(to_cw) = navmesh.node_weight(edge.target()).unwrap().maybe_cw { - if to_cw { - to -= [0.0, 150.0].into(); - } else { - to += [0.0, 150.0].into(); - } - } - - let stroke = 'blk: { - if let (Some(source_pos), Some(target_pos)) = ( - activity.maybe_navcord().map(|navcord| - navcord.path - .iter() - .position(|node| *node == edge.source())).flatten(), - activity.maybe_navcord().map(|navcord| - navcord.path - .iter() - .position(|node| *node == edge.target())).flatten(), - ) { - if target_pos == source_pos + 1 - || source_pos == target_pos + 1 - { - break 'blk egui::Stroke::new( - 5.0, - egui::Color32::from_rgb(250, 250, 0), - ); - } - } - - egui::Stroke::new(1.0, egui::Color32::from_rgb(125, 125, 125)) + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .normal }; - painter.paint_edge(from, to, stroke); + painter.paint_primitive(&shape, color); + } + + for poly in board.layout().layer_poly_nodes(i) { + let color = if overlay + .selection() + .contains_node(board, GenericNode::Compound(poly.into())) + { + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .highlighted + } else { + config + .colors(ctx) + .layers + .color(board.layout().rules().layer_layername(i)) + .normal + }; + + painter.paint_polygon( + &board.layout().poly(poly).shape().polygon, + color, + ) } } } - } - if menu_bar.show_bboxes { - let root_bbox3d = board.layout().drawing().rtree().root().envelope(); + if menu_bar.show_ratsnest { + let graph = overlay.ratsnest().graph(); + for edge in graph.edge_references() { + let from = graph.node_weight(edge.source()).unwrap().pos; + let to = graph.node_weight(edge.target()).unwrap().pos; - let root_bbox = AABB::<[f64; 2]>::from_corners([root_bbox3d.lower()[0], root_bbox3d.lower()[1]].into(), [root_bbox3d.upper()[0], root_bbox3d.upper()[1]].into()); - painter.paint_bbox(root_bbox); - } - - if let Some(activity) = &mut workspace.interactor.maybe_activity() { - for ghost in activity.ghosts().iter() { - painter.paint_primitive(&ghost, egui::Color32::from_rgb(75, 75, 150)); - } - - if let Some(navmesh) = activity.maybe_navmesh() { - if menu_bar.show_origin_destination { - let (origin, destination) = (navmesh.origin(), navmesh.destination()); - painter.paint_dot( - Circle { - pos: board.layout().drawing().primitive(origin).shape().center(), - r: 150.0, - }, - egui::Color32::from_rgb(255, 255, 100), - ); - painter.paint_dot( - Circle { - pos: board.layout().drawing().primitive(destination).shape().center(), - r: 150.0, - }, - egui::Color32::from_rgb(255, 255, 100), + painter.paint_edge( + from, + to, + egui::Stroke::new(1.0, egui::Color32::from_rgb(90, 90, 200)), ); } } + + if menu_bar.show_navmesh { + if let Some(activity) = workspace.interactor.maybe_activity() { + if let Some(navmesh) = activity.maybe_navmesh() { + for edge in navmesh.edge_references() { + let mut from = PrimitiveIndex::from( + navmesh.node_weight(edge.source()).unwrap().node, + ) + .primitive(board.layout().drawing()) + .shape() + .center(); + let mut to = PrimitiveIndex::from( + navmesh.node_weight(edge.target()).unwrap().node, + ) + .primitive(board.layout().drawing()) + .shape() + .center(); + + if let Some(from_cw) = + navmesh.node_weight(edge.source()).unwrap().maybe_cw + { + if from_cw { + from -= [0.0, 150.0].into(); + } else { + from += [0.0, 150.0].into(); + } + } + + if let Some(to_cw) = + navmesh.node_weight(edge.target()).unwrap().maybe_cw + { + if to_cw { + to -= [0.0, 150.0].into(); + } else { + to += [0.0, 150.0].into(); + } + } + + let stroke = + 'blk: { + if let (Some(source_pos), Some(target_pos)) = ( + activity + .maybe_navcord() + .map(|navcord| { + navcord.path.iter().position(|node| { + *node == edge.source() + }) + }) + .flatten(), + activity + .maybe_navcord() + .map(|navcord| { + navcord.path.iter().position(|node| { + *node == edge.target() + }) + }) + .flatten(), + ) { + if target_pos == source_pos + 1 + || source_pos == target_pos + 1 + { + break 'blk egui::Stroke::new( + 5.0, + egui::Color32::from_rgb(250, 250, 0), + ); + } + } + + egui::Stroke::new( + 1.0, + egui::Color32::from_rgb(125, 125, 125), + ) + }; + + painter.paint_edge(from, to, stroke); + } + } + } + } + + if menu_bar.show_bboxes { + let root_bbox3d = board.layout().drawing().rtree().root().envelope(); + + let root_bbox = AABB::<[f64; 2]>::from_corners( + [root_bbox3d.lower()[0], root_bbox3d.lower()[1]].into(), + [root_bbox3d.upper()[0], root_bbox3d.upper()[1]].into(), + ); + painter.paint_bbox(root_bbox); + } + + if let Some(activity) = workspace.interactor.maybe_activity() { + for ghost in activity.ghosts().iter() { + painter + .paint_primitive(&ghost, egui::Color32::from_rgb(75, 75, 150)); + } + + if let Some(navmesh) = activity.maybe_navmesh() { + if menu_bar.show_origin_destination { + let (origin, destination) = + (navmesh.origin(), navmesh.destination()); + painter.paint_dot( + Circle { + pos: board + .layout() + .drawing() + .primitive(origin) + .shape() + .center(), + r: 150.0, + }, + egui::Color32::from_rgb(255, 255, 100), + ); + painter.paint_dot( + Circle { + pos: board + .layout() + .drawing() + .primitive(destination) + .shape() + .center(), + r: 150.0, + }, + egui::Color32::from_rgb(255, 255, 100), + ); + } + } + } + + if self.scheduled_zoom_to_fit { + let root_bbox = workspace + .interactor + .invoker() + .autorouter() + .board() + .layout() + .drawing() + .rtree() + .root() + .envelope(); + + let root_bbox_width = root_bbox.upper()[0] - root_bbox.lower()[0]; + let root_bbox_height = root_bbox.upper()[1] - root_bbox.lower()[1]; + + self.transform.scaling = 0.8 + * if root_bbox_width / root_bbox_height + >= (viewport_rect.width() as f64) + / (viewport_rect.height() as f64) + { + viewport_rect.width() / root_bbox_width as f32 + } else { + viewport_rect.height() / root_bbox_height as f32 + }; + + self.transform.translation = egui::Vec2::new( + viewport_rect.center()[0] as f32, + viewport_rect.center()[1] as f32, + ) - (self.transform.scaling + * egui::Pos2::new( + root_bbox.center()[0] as f32, + -root_bbox.center()[1] as f32, + )) + .to_vec2(); + } + + self.scheduled_zoom_to_fit = false; } - if self.scheduled_zoom_to_fit { - let root_bbox = workspace.interactor.invoker() - .autorouter() - .board() - .layout() - .drawing() - .rtree() - .root() - .envelope(); - - let root_bbox_width = root_bbox.upper()[0] - root_bbox.lower()[0]; - let root_bbox_height = root_bbox.upper()[1] - root_bbox.lower()[1]; - - self.transform.scaling = 0.8 * if root_bbox_width / root_bbox_height - >= (viewport_rect.width() as f64) / (viewport_rect.height() as f64) - { - viewport_rect.width() / root_bbox_width as f32 - } else { - viewport_rect.height() / root_bbox_height as f32 - }; - - self.transform.translation = egui::Vec2::new( - viewport_rect.center()[0] as f32, - viewport_rect.center()[1] as f32, - ) - (self.transform.scaling - * egui::Pos2::new(root_bbox.center()[0] as f32, -root_bbox.center()[1] as f32)) - .to_vec2(); - } - - self.scheduled_zoom_to_fit = false; - } - - viewport_rect + viewport_rect + }) }) - }).inner.inner + .inner + .inner } } diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 8710ed9..f693756 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -12,6 +12,8 @@ tr-menu-edit = Edit tr-menu-edit-undo = Undo tr-menu-edit-redo = Redo tr-menu-edit-abort = Abort +tr-menu-edit-select-all = Select All +tr-menu-edit-unselect-all = Unselect All 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..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,37 @@ 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); + } 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 +198,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() }