Find pin selector of what is under mouse pointer

This commit is contained in:
Mikolaj Wielgus 2026-03-14 22:04:08 +01:00
parent 6992369041
commit 13c7bbb061
9 changed files with 180 additions and 79 deletions

View File

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use egui::Pos2;
use topola::Vector2;
use crate::{display::Display, workspace::Workspace};
@ -23,25 +24,74 @@ impl Viewport {
pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut Workspace>) {
egui::CentralPanel::default().show(ctx, |ui| {
let mut scene_rect = self.scene_rect.clone();
egui::Frame::canvas(ui.style()).show(ui, |ui| {
let zoom_range = 0.00001..=10000.0;
egui::Scene::new()
.zoom_range(0.00001..=10000.0)
.show(ui, &mut scene_rect, |ui| {
if let Some(ref workspace) = workspace {
let mut display = Display::new();
display.update(ctx, ui, &self, workspace);
}
});
let viewport_rect = ui.available_rect_before_wrap();
let mut scene_rect = self.scene_rect.clone();
self.scene_rect = scene_rect;
egui::Scene::new()
.zoom_range(zoom_range.clone())
.show(ui, &mut scene_rect, |ui| {
if let Some(ref workspace) = workspace {
let mut display = Display::new();
display.update(ctx, ui, &self, workspace);
}
});
if let Some(workspace) = workspace {
self.zoom_to_fit_if_scheduled(workspace);
}
self.scene_rect = scene_rect;
let scene_to_viewport =
Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into());
let response = ui.interact(viewport_rect, ui.id(), egui::Sense::click_and_drag());
let pointer_scene_pos = scene_to_viewport.inverse()
* (response.interact_pointer_pos().unwrap_or_else(|| {
ctx.input(|i| i.pointer.interact_pos().unwrap_or_default())
}));
if let Some(workspace) = workspace {
dbg!(workspace.navmesher_board.board().point_pin_selector(
0,
Vector2::new(pointer_scene_pos.x as i64, pointer_scene_pos.y as i64)
));
self.zoom_to_fit_if_scheduled(workspace);
}
})
});
}
/// Copied from egui/containers/scene.rs and modified.
///
/// Creates a transformation that fits a given scene rectangle into the available screen size.
///
/// The resulting visual scene bounds can be larger, due to letterboxing.
///
/// Returns the transformation from `scene` to `global` coordinates.
fn fit_to_rect_in_scene(
rect_in_viewport: egui::Rect,
rect_in_scene: egui::Rect,
zoom_range: egui::Rangef,
) -> egui::emath::TSTransform {
// Compute the scale factor to fit the bounding rectangle into the available screen size:
let scale = rect_in_viewport.size() / rect_in_scene.size();
// Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
let scale = scale.min_elem();
// Clamp scale to what is allowed
let scale = zoom_range.clamp(scale);
// Compute the translation to center the bounding rect in the screen:
let center_in_global = rect_in_viewport.center().to_vec2();
let center_scene = rect_in_scene.center().to_vec2();
// Set the transformation to scale and then translate to center.
egui::emath::TSTransform::from_translation(center_in_global - scale * center_scene)
* egui::emath::TSTransform::from_scaling(scale)
}
fn zoom_to_fit_if_scheduled(&mut self, workspace: &Workspace) {
if self.scheduled_zoom_to_fit {
self.scene_rect = Self::boundary_bounding_box(workspace);

View File

@ -2,13 +2,14 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use topola::{Board, NavmesherBoard};
use topola::{Board, NavmesherBoard, PinSelection};
use crate::{appearance_panel::AppearancePanel, translator::Translator};
pub struct Workspace {
pub navmesher_board: NavmesherBoard,
pub appearance_panel: AppearancePanel,
pub pin_selection: PinSelection,
}
impl Workspace {
@ -18,6 +19,7 @@ impl Workspace {
Self {
navmesher_board: NavmesherBoard::with_board(board),
appearance_panel,
pin_selection: PinSelection::new(),
}
}

View File

@ -8,41 +8,43 @@ use undoredo::{ApplyDelta, Delta, FlushDelta};
use crate::{
layout::{Layout, LayoutHalfDelta, NetId, PinId},
math::Vector2,
primitives::{Joint, JointId, Polygon, PolygonId, Segment, SegmentId, Via, ViaId},
selection::PinSelector,
};
#[derive(Clone, Debug, Getters)]
pub struct Board {
layout: Layout,
#[getter(skip)]
pin_names: BiBTreeMap<PinId, String>,
#[getter(skip)]
layer_names: BiBTreeMap<usize, String>,
#[getter(skip)]
net_names: BiBTreeMap<NetId, String>,
#[getter(skip)]
pin_names: BiBTreeMap<PinId, String>,
}
impl Board {
pub fn new(boundary: Vec<[i64; 2]>, layer_count: usize) -> Self {
pub fn new(boundary: Vec<Vector2<i64>>, layer_count: usize) -> Self {
Self {
layout: Layout::new(boundary, layer_count),
layout: Layout::new(boundary.into_iter().map(Into::into).collect(), layer_count),
pin_names: BiBTreeMap::new(),
layer_names: BiBTreeMap::new(),
net_names: BiBTreeMap::new(),
pin_names: BiBTreeMap::new(),
}
}
pub fn with_names(
boundary: Vec<[i64; 2]>,
boundary: Vec<Vector2<i64>>,
layer_count: usize,
layer_names: BiBTreeMap<usize, String>,
net_names: BiBTreeMap<NetId, String>,
) -> Self {
Self {
layout: Layout::new(boundary, layer_count),
layout: Layout::new(boundary.into_iter().map(Into::into).collect(), layer_count),
pin_names: BiBTreeMap::new(),
layer_names,
net_names,
pin_names: BiBTreeMap::new(),
}
}
@ -73,20 +75,75 @@ impl Board {
self.layout.add_polygon(polygon)
}
pub fn joint_pin_selector(&self, joint_id: JointId) -> Option<PinSelector> {
let joint = self.layout.joint(joint_id);
Some(PinSelector {
pin: self.pin_name(joint.pin?)?.to_string(),
layer: self.layer_name(joint.layer)?.to_string(),
})
}
pub fn segment_pin_selector(&self, segment_id: SegmentId) -> Option<PinSelector> {
let segment = self.layout.segment(segment_id);
Some(PinSelector {
pin: self.pin_name(segment.pin?)?.to_string(),
layer: self.layer_name(segment.layer)?.to_string(),
})
}
// TODO: Vias.
pub fn polygon_pin_selector(&self, polygon_id: PolygonId) -> Option<PinSelector> {
let polygon = self.layout.polygon(polygon_id);
Some(PinSelector {
pin: self.pin_name(polygon.pin?)?.to_string(),
layer: self.layer_name(polygon.layer)?.to_string(),
})
}
pub fn point_pin_selector(&self, layer: usize, point: Vector2<i64>) -> Option<PinSelector> {
if let Some(joint_id) = self.layout.locate_joints_at_point(layer, point).next() {
return self.joint_pin_selector(joint_id);
}
if let Some(segment_id) = self.layout.locate_segments_at_point(layer, point).next() {
return self.segment_pin_selector(segment_id);
}
// TODO: Vias.
if let Some(polygon_id) = self.layout.locate_polygons_at_point(layer, point).next() {
return self.polygon_pin_selector(polygon_id);
}
None
}
pub fn pin_name(&self, pin: PinId) -> Option<&str> {
self.pin_names.get_by_left(&pin).map(String::as_str)
}
pub fn pin_id(&self, pin_name: &str) -> Option<PinId> {
self.pin_names.get_by_right(pin_name).copied()
}
pub fn layer_name(&self, layer: usize) -> Option<&str> {
self.layer_names.get_by_left(&layer).map(String::as_str)
}
pub fn layer_id(&self, name: &str) -> Option<usize> {
self.layer_names.get_by_right(name).copied()
pub fn layer_id(&self, layer_name: &str) -> Option<usize> {
self.layer_names.get_by_right(layer_name).copied()
}
pub fn net_name(&self, net: NetId) -> Option<&str> {
self.net_names.get_by_left(&net).map(String::as_str)
}
pub fn net_id(&self, name: &str) -> Option<NetId> {
self.net_names.get_by_right(name).copied()
pub fn net_id(&self, net_name: &str) -> Option<NetId> {
self.net_names.get_by_right(net_name).copied()
}
}

View File

@ -246,8 +246,20 @@ impl Layout {
})
}
pub fn pin(&self, pin: PinId) -> &Pin {
&self.pins[pin.id()]
pub fn joint(&self, joint_id: JointId) -> &Joint {
self.joints.get(&joint_id.id()).unwrap()
}
pub fn segment(&self, segment_id: SegmentId) -> &Segment {
self.segments.get(&segment_id.id()).unwrap()
}
pub fn polygon(&self, polygon_id: PolygonId) -> &Polygon {
self.polygons.get(&polygon_id.id()).unwrap()
}
pub fn pin(&self, pin_id: PinId) -> &Pin {
&self.pins[pin_id.id()]
}
}

View File

@ -15,3 +15,4 @@ pub use crate::layout::Layout;
pub use crate::math::Vector2;
pub use crate::navmesher::NavmesherBoard;
pub use crate::primitives::{Joint, JointId, Polygon, PolygonId, Segment, SegmentId, Via, ViaId};
pub use crate::selection::{PinSelection, PinSelector};

View File

@ -25,21 +25,35 @@ impl<T: Copy> From<Vector2<T>> for [T; 2] {
}
}
// Check if the point (px, py) is inside a polygon using the ray-casting
// algorithm.
// Checks if the point (px, py) is inside a polygon using the ray-casting
// algorithm. Division is not used to avoid integer truncation errors.
macro_rules! impl_inside_polygon {
($type:ty) => {
impl Vector2<$type> {
pub fn inside_polygon(&self, polygon: &[Vector2<$type>]) -> bool {
let mut inside = false;
let n = polygon.len();
let px = &self.x;
let py = &self.y;
let px = self.x;
let py = self.y;
let mut p1 = &polygon[n - 1];
for p2 in polygon.iter() {
if (*py > p1.y) != (*py > p2.y) {
if *px < (p2.x - p1.x) * (*py - p1.y) / (p2.y - p1.y) + p1.x {
let dy = p2.y - p1.y;
let zero = 0 as $type;
if dy != zero && (py > p1.y) != (py > p2.y) {
let dx = p2.x - p1.x;
let t = py - p1.y;
let s = px - p1.x;
let crosses = if dy > zero {
s * dy < dx * t
} else {
s * dy > dx * t
};
if crosses {
inside = !inside;
}
}
@ -59,18 +73,11 @@ impl_inside_polygon!(i64);
/// Returns the four vertices of a segment inflated by `half_width`, forming a convex
/// quadrilateral. The segment goes from (x1, y1) to (x2, y2).
pub fn inflated_segment(
x1: i64,
y1: i64,
x2: i64,
y2: i64,
half_width: u64,
) -> [Vector2<i64>; 4] {
pub fn inflated_segment(x1: i64, y1: i64, x2: i64, y2: i64, half_width: u64) -> [Vector2<i64>; 4] {
let dx = x2 - x1;
let dy = y2 - y1;
let approx_len =
std::cmp::max(dx.abs(), dy.abs()) + 3 * std::cmp::min(dx.abs(), dy.abs()) / 8;
let approx_len = std::cmp::max(dx.abs(), dy.abs()) + 3 * std::cmp::min(dx.abs(), dy.abs()) / 8;
// Perpendicular vector scaled to half-width.
let px = -dy * (half_width as i64) / approx_len;

View File

@ -54,13 +54,6 @@ impl Joint {
(point.x - self.position.x).pow(2) as u64 + (point.y - self.position.y).pow(2) as u64
<= self.radius.pow(2)
}
pub fn pin_selector(&self) -> Option<PinSelector> {
Some(PinSelector {
pin: self.pin?,
layer: self.layer,
})
}
}
#[derive(
@ -85,15 +78,6 @@ pub struct Segment {
pub pin: Option<PinId>,
}
impl Segment {
pub fn pin_selector(&self) -> Option<PinSelector> {
Some(PinSelector {
pin: self.pin?,
layer: self.layer,
})
}
}
#[derive(
Clone, Constructor, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize,
)]
@ -120,13 +104,6 @@ impl Via {
/*pub fn bbox(&self) -> Rectangle<[i64; 3]> {
//
}*/
pub fn pin_selector(&self) -> Option<PinSelector> {
Some(PinSelector {
pin: self.pin?,
layer: self.layer,
})
}
}
#[derive(
@ -163,13 +140,6 @@ impl Polygon {
}
pub fn contains_point(&self, point: Vector2<i64>) -> bool {
point.inside_polygon(&self.vertices)
}
pub fn pin_selector(&self) -> Option<PinSelector> {
Some(PinSelector {
pin: self.pin?,
layer: self.layer,
})
dbg!(point.inside_polygon(&self.vertices))
}
}

View File

@ -6,18 +6,20 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::layout::PinId;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct PinSelector {
pub pin: PinId,
pub layer: usize,
pub pin: String,
pub layer: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PinSelection(pub BTreeSet<PinSelector>);
impl PinSelection {
pub fn new() -> Self {
Self(BTreeSet::new())
}
pub fn toggle(&mut self, pin_selector: PinSelector) {
if self.0.contains(&pin_selector) {
self.0.remove(&pin_selector);

View File

@ -60,7 +60,7 @@ impl Board {
.into_iter()
.skip(1)
.rev()
.map(|p| [p.x as i64, p.y as i64])
.map(|p| Vector2::new(p.x as i64, p.y as i64))
.collect(),
dsn.pcb.structure.layers.len(),
layer_names,