feat: implement bounding box selection (#139)

Fixes #138

Reviewed-on: https://codeberg.org/topola/topola/pulls/139
Co-authored-by: Alain Emilia Anna Zscheile <fogti+devel@ytrizja.de>
Co-committed-by: Alain Emilia Anna Zscheile <fogti+devel@ytrizja.de>
This commit is contained in:
Alain Emilia Anna Zscheile 2025-01-02 22:12:11 +00:00 committed by mikolaj
parent bbf54c9eb5
commit 53b964b865
12 changed files with 528 additions and 256 deletions

View File

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

View File

@ -14,3 +14,7 @@ serde.workspace = true
specctra_derive.path = "../specctra_derive" specctra_derive.path = "../specctra_derive"
thiserror.workspace = true thiserror.workspace = true
utf8-chars = "3.0" 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() 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 { 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 { pub fn consume_key_triggered(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> bool {
self.consume_key(ctx, ui); self.consume_key(ctx, ui);
self.triggered() self.triggered
} }
fn consume_key(&mut self, ctx: &egui::Context, _ui: &mut egui::Ui) { fn consume_key(&mut self, ctx: &egui::Context, _ui: &mut egui::Ui) {

View File

@ -78,6 +78,8 @@ pub struct EditActions {
pub undo: Trigger, pub undo: Trigger,
pub redo: Trigger, pub redo: Trigger,
pub abort: Trigger, pub abort: Trigger,
pub select_all: Trigger,
pub unselect_all: Trigger,
pub remove_bands: Trigger, pub remove_bands: Trigger,
} }
@ -102,6 +104,18 @@ impl EditActions {
egui::Key::Escape, egui::Key::Escape,
) )
.into_trigger(), .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( remove_bands: Action::new(
tr.text("tr-menu-edit-remove-bands"), tr.text("tr-menu-edit-remove-bands"),
egui::Modifiers::NONE, egui::Modifiers::NONE,
@ -128,6 +142,11 @@ impl EditActions {
ui.separator(); ui.separator();
self.select_all.button(ctx, ui);
self.unselect_all.button(ctx, ui);
ui.separator();
//ui.add_enabled_ui(workspace_activities_enabled, |ui| { //ui.add_enabled_ui(workspace_activities_enabled, |ui| {
self.remove_bands.button(ctx, ui); self.remove_bands.button(ctx, ui);
//}); //});
@ -174,7 +193,7 @@ impl RouteActions {
autoroute: Action::new( autoroute: Action::new(
tr.text("tr-menu-route-autoroute"), tr.text("tr-menu-route-autoroute"),
egui::Modifiers::CTRL, egui::Modifiers::CTRL,
egui::Key::A, egui::Key::R,
) )
.into_trigger(), .into_trigger(),
} }

View File

@ -232,6 +232,12 @@ impl MenuBar {
workspace.interactor.redo(); workspace.interactor.redo();
} else if actions.edit.abort.consume_key_triggered(ctx, ui) { } else if actions.edit.abort.consume_key_triggered(ctx, ui) {
workspace.interactor.abort(); 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( } else if actions.place.place_via.consume_key_enabled(
ctx, ctx,
ui, ui,

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use geo::Point; use geo::Point;
use rstar::AABB; use rstar::{Point as _, AABB};
use spade::InsertionError; use spade::InsertionError;
use topola::{ use topola::{
@ -18,21 +18,32 @@ use topola::{
layout::{ layout::{
poly::{MakePolyShape, PolyWeight}, poly::{MakePolyShape, PolyWeight},
via::ViaWeight, via::ViaWeight,
CompoundWeight, NodeIndex, CompoundWeight, Layout, NodeIndex,
}, },
}; };
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelectionMode {
Addition,
Substitution,
Toggling,
}
pub struct Overlay { pub struct Overlay {
ratsnest: Ratsnest, ratsnest: Ratsnest,
selection: Selection, selection: Selection,
reselect_bbox: Option<(SelectionMode, Point)>,
active_layer: usize, active_layer: usize,
} }
const INF: f64 = f64::INFINITY;
impl Overlay { impl Overlay {
pub fn new(board: &Board<impl AccessMesadata>) -> Result<Self, InsertionError> { pub fn new(board: &Board<impl AccessMesadata>) -> Result<Self, InsertionError> {
Ok(Self { Ok(Self {
ratsnest: Ratsnest::new(board.layout())?, ratsnest: Ratsnest::new(board.layout())?,
selection: Selection::new(), selection: Selection::new(),
reselect_bbox: None,
active_layer: 0, active_layer: 0,
}) })
} }
@ -41,11 +52,64 @@ impl Overlay {
core::mem::replace(&mut self.selection, Selection::new()) core::mem::replace(&mut self.selection, Selection::new())
} }
pub fn clear_selection(&mut self) { pub fn select_all(&mut self, board: &Board<impl AccessMesadata>) {
self.select_all_in_bbox(board, &AABB::from_corners([-INF, -INF], [INF, INF]));
}
pub fn unselect_all(&mut self) {
self.selection = Selection::new(); self.selection = Selection::new();
self.reselect_bbox = None;
}
pub fn drag_start(
&mut self,
board: &Board<impl AccessMesadata>,
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<impl AccessMesadata>, 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<impl AccessMesadata>, at: Point) { pub fn click(&mut self, board: &Board<impl AccessMesadata>, 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 let geoms: Vec<_> = board
.layout() .layout()
.drawing() .drawing()
@ -57,61 +121,22 @@ impl Overlay {
.collect(); .collect();
if let Some(geom) = geoms.iter().find(|&&geom| { if let Some(geom) = geoms.iter().find(|&&geom| {
self.contains_point(board, geom.data, at) board.layout().node_shape(geom.data).contains_point(at)
&& match geom.data { && board
NodeIndex::Primitive(primitive) => { .layout()
primitive.primitive(board.layout().drawing()).layer() == self.active_layer .is_node_in_layer(geom.data, 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
}
}
}
}
}) { }) {
self.selection.toggle_at_node(board, geom.data); self.selection.toggle_at_node(board, geom.data);
} }
} }
fn contains_point( pub fn select_all_in_bbox(
&self, &mut self,
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
node: NodeIndex, aabb: &AABB<[f64; 2]>,
p: Point, ) {
) -> bool { self.selection
let shape: Shape = match node { .select_all_in_bbox(board, aabb, self.active_layer);
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 ratsnest(&self) -> &Ratsnest { pub fn ratsnest(&self) -> &Ratsnest {
@ -121,4 +146,14 @@ impl Overlay {
pub fn selection(&self) -> &Selection { pub fn selection(&self) -> &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()]),
)
})
}
} }

View File

@ -59,6 +59,10 @@ impl<'a> Painter<'a> {
} }
pub fn paint_bbox(&mut self, bbox: AABB<[f64; 2]>) { 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 { let rect = egui::epaint::Rect {
min: [bbox.lower()[0] as f32, -bbox.upper()[1] as f32].into(), min: [bbox.lower()[0] as f32, -bbox.upper()[1] as f32].into(),
max: [bbox.upper()[0] as f32, -bbox.lower()[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.ui.painter().add(egui::Shape::rect_stroke(
self.transform * rect, self.transform * rect,
egui::Rounding::ZERO, egui::Rounding::ZERO,
egui::Stroke::new(1.0, egui::Color32::GRAY), egui::Stroke::new(1.0, color),
)); ));
} }

View File

@ -45,232 +45,320 @@ impl Viewport {
menu_bar: &MenuBar, menu_bar: &MenuBar,
maybe_workspace: Option<&mut Workspace>, maybe_workspace: Option<&mut Workspace>,
) -> egui::Rect { ) -> egui::Rect {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default()
egui::Frame::canvas(ui.style()).show(ui, |ui| { .show(ctx, |ui| {
ui.ctx().request_repaint(); egui::Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint();
let (_id, viewport_rect) = ui.allocate_space(ui.available_size()); 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 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; let old_scaling = self.transform.scaling;
self.transform.scaling *= ctx.input(|i| i.zoom_delta()); self.transform.scaling *= ctx.input(|i| i.zoom_delta());
self.transform.translation += latest_pos.to_vec2() * (old_scaling - self.transform.scaling); self.transform.translation +=
self.transform.translation += ctx.input(|i| i.smooth_scroll_delta); 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 { if let Some(workspace) = maybe_workspace {
let layers = &mut workspace.appearance_panel; let layers = &mut workspace.appearance_panel;
let overlay = &mut workspace.overlay; 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 response.clicked_by(egui::PointerButton::Primary) {
if menu_bar.is_placing_via { if menu_bar.is_placing_via {
workspace.interactor.execute( workspace.interactor.execute(Command::PlaceVia(ViaWeight {
Command::PlaceVia(ViaWeight {
from_layer: 0, from_layer: 0,
to_layer: 0, to_layer: 0,
circle: Circle { circle: Circle {
pos: point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, pos: latest_point,
r: menu_bar.autorouter_options.router_options.routed_band_width / 2.0, r: menu_bar
.autorouter_options
.router_options
.routed_band_width
/ 2.0,
}, },
maybe_net: Some(1234), maybe_net: Some(1234),
}), }));
); } else {
} else { overlay.click(board, latest_point);
overlay.click( }
workspace.interactor.invoker().autorouter().board(), } else if response.drag_started_by(egui::PointerButton::Primary) {
point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, 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() { for i in (0..layers.visible.len()).rev() {
if layers.visible[i] { if layers.visible[i] {
for primitive in board.layout().drawing().layer_primitive_nodes(i) { for primitive in board.layout().drawing().layer_primitive_nodes(i) {
let shape = primitive.primitive(board.layout().drawing()).shape(); let shape =
primitive.primitive(board.layout().drawing()).shape();
let color = if overlay let color = if overlay
.selection() .selection()
.contains_node(board, GenericNode::Primitive(primitive)) .contains_node(board, GenericNode::Primitive(primitive))
{ {
config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).highlighted config
} else if let Some(activity) = &mut workspace.interactor.maybe_activity() { .colors(ctx)
if activity.obstacles().contains(&primitive) { .layers
config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).highlighted .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 { } else {
config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).normal config
} .colors(ctx)
} else { .layers
config.colors(ctx).layers.color(board.layout().rules().layer_layername(i)).normal .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))
}; };
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 { if menu_bar.show_ratsnest {
let root_bbox3d = board.layout().drawing().rtree().root().envelope(); 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_edge(
painter.paint_bbox(root_bbox); from,
} to,
egui::Stroke::new(1.0, egui::Color32::from_rgb(90, 90, 200)),
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),
); );
} }
} }
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 { viewport_rect
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
}) })
}).inner.inner .inner
.inner
} }
} }

View File

@ -12,6 +12,8 @@ tr-menu-edit = Edit
tr-menu-edit-undo = Undo tr-menu-edit-undo = Undo
tr-menu-edit-redo = Redo tr-menu-edit-redo = Redo
tr-menu-edit-abort = Abort 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-edit-remove-bands = Remove Bands
tr-menu-view = View tr-menu-view = View

View File

@ -4,6 +4,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use rstar::AABB;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@ -145,6 +146,37 @@ impl Selection {
Self::default() Self::default()
} }
pub fn select_all_in_bbox(
&mut self,
board: &Board<impl AccessMesadata>,
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<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) { pub fn toggle_at_node(&mut self, board: &Board<impl AccessMesadata>, node: NodeIndex) {
if let Some(selector) = PinSelector::try_from_node(board, node) { if let Some(selector) = PinSelector::try_from_node(board, node) {
if self.pin_selection.0.contains(&selector) { if self.pin_selection.0.contains(&selector) {
@ -166,3 +198,10 @@ impl Selection {
|| self.band_selection.contains_node(board, node) || 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, cane::Cane,
dot::{DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, LooseDotIndex, LooseDotWeight}, dot::{DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, LooseDotIndex, LooseDotWeight},
gear::GearIndex, gear::GearIndex,
graph::{GetMaybeNet, PrimitiveIndex, PrimitiveWeight}, graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex, PrimitiveWeight},
primitive::MakePrimitiveShape,
rules::AccessRules, rules::AccessRules,
seg::{ seg::{
FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex, FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex,
@ -23,10 +24,10 @@ use crate::{
}, },
Drawing, DrawingEdit, DrawingException, Infringement, Drawing, DrawingEdit, DrawingException, Infringement,
}, },
geometry::{edit::ApplyGeometryEdit, GenericNode}, geometry::{edit::ApplyGeometryEdit, shape::Shape, GenericNode},
graph::{GenericIndex, GetPetgraphIndex}, graph::{GenericIndex, GetPetgraphIndex},
layout::{ layout::{
poly::{Poly, PolyWeight}, poly::{MakePolyShape, Poly, PolyWeight},
via::{Via, ViaWeight}, via::{Via, ViaWeight},
}, },
}; };
@ -317,6 +318,67 @@ impl<R: AccessRules> Layout<R> {
.compound_members(GenericIndex::new(poly.petgraph_index())) .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 { pub fn rules(&self) -> &R {
self.drawing.rules() self.drawing.rules()
} }