From 13c7bbb06143de6c7eeea06755805d6718d1a495 Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Sat, 14 Mar 2026 22:04:08 +0100 Subject: [PATCH] Find pin selector of what is under mouse pointer --- topola-egui/src/viewport.rs | 76 +++++++++++++++++++++++++++------ topola-egui/src/workspace.rs | 4 +- topola/src/board.rs | 81 ++++++++++++++++++++++++++++++------ topola/src/layout.rs | 16 ++++++- topola/src/lib.rs | 1 + topola/src/math.rs | 37 +++++++++------- topola/src/primitives.rs | 32 +------------- topola/src/selection.rs | 10 +++-- topola/src/specctra.rs | 2 +- 9 files changed, 180 insertions(+), 79 deletions(-) diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index 877470f..8420e55 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -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); diff --git a/topola-egui/src/workspace.rs b/topola-egui/src/workspace.rs index 9f320c0..66cece9 100644 --- a/topola-egui/src/workspace.rs +++ b/topola-egui/src/workspace.rs @@ -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(), } } diff --git a/topola/src/board.rs b/topola/src/board.rs index bd5cacf..315307a 100644 --- a/topola/src/board.rs +++ b/topola/src/board.rs @@ -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, + #[getter(skip)] layer_names: BiBTreeMap, #[getter(skip)] net_names: BiBTreeMap, - #[getter(skip)] - pin_names: BiBTreeMap, } impl Board { - pub fn new(boundary: Vec<[i64; 2]>, layer_count: usize) -> Self { + pub fn new(boundary: Vec>, 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>, layer_count: usize, layer_names: BiBTreeMap, net_names: BiBTreeMap, ) -> 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 { + 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 { + 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 { + 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) -> Option { + 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 { + 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 { - self.layer_names.get_by_right(name).copied() + pub fn layer_id(&self, layer_name: &str) -> Option { + 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 { - self.net_names.get_by_right(name).copied() + pub fn net_id(&self, net_name: &str) -> Option { + self.net_names.get_by_right(net_name).copied() } } diff --git a/topola/src/layout.rs b/topola/src/layout.rs index 09767f6..c7943a2 100644 --- a/topola/src/layout.rs +++ b/topola/src/layout.rs @@ -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()] } } diff --git a/topola/src/lib.rs b/topola/src/lib.rs index 3ad740e..a5e7b48 100644 --- a/topola/src/lib.rs +++ b/topola/src/lib.rs @@ -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}; diff --git a/topola/src/math.rs b/topola/src/math.rs index 0b2ed0a..66ddcbe 100644 --- a/topola/src/math.rs +++ b/topola/src/math.rs @@ -25,21 +25,35 @@ impl From> 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; 4] { +pub fn inflated_segment(x1: i64, y1: i64, x2: i64, y2: i64, half_width: u64) -> [Vector2; 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; diff --git a/topola/src/primitives.rs b/topola/src/primitives.rs index c83142a..0043f7d 100644 --- a/topola/src/primitives.rs +++ b/topola/src/primitives.rs @@ -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 { - Some(PinSelector { - pin: self.pin?, - layer: self.layer, - }) - } } #[derive( @@ -85,15 +78,6 @@ pub struct Segment { pub pin: Option, } -impl Segment { - pub fn pin_selector(&self) -> Option { - 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 { - Some(PinSelector { - pin: self.pin?, - layer: self.layer, - }) - } } #[derive( @@ -163,13 +140,6 @@ impl Polygon { } pub fn contains_point(&self, point: Vector2) -> bool { - point.inside_polygon(&self.vertices) - } - - pub fn pin_selector(&self) -> Option { - Some(PinSelector { - pin: self.pin?, - layer: self.layer, - }) + dbg!(point.inside_polygon(&self.vertices)) } } diff --git a/topola/src/selection.rs b/topola/src/selection.rs index fef8a4c..596ceaf 100644 --- a/topola/src/selection.rs +++ b/topola/src/selection.rs @@ -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); 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); diff --git a/topola/src/specctra.rs b/topola/src/specctra.rs index 904a3ed..8002d09 100644 --- a/topola/src/specctra.rs +++ b/topola/src/specctra.rs @@ -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,