Compare commits

..

4 Commits

Author SHA1 Message Date
Mikolaj Wielgus 85f33f1a6c Exactly filter upon location after bbox hit 2026-05-28 02:17:03 +02:00
Mikolaj Wielgus e1795413bd Actually display the drag selection box, and some fixes 2026-05-27 23:45:21 +02:00
Mikolaj Wielgus 1a2c4aeec2 Make selection responsibility of `topola`, not `topola-egui`, with new `Workspace` struct
Now use `MasterInteractor`.
2026-05-27 23:16:29 +02:00
Mikolaj Wielgus da7585b135 Add master interactor to rule all interactors (WIP, not used yet) 2026-05-27 17:07:18 +02:00
32 changed files with 952 additions and 480 deletions

View File

@ -8,7 +8,9 @@ use specctra::{error::ParseErrorContext, structure::DsnFile};
use topola::Board;
use unic_langid::langid;
use crate::{menu_bar::MenuBar, translator::Translator, viewport::Viewport, workspace::Workspace};
use crate::{
menu_bar::MenuBar, translator::Translator, viewport::Viewport, workspace::GuiWorkspace,
};
pub struct App {
translator: Translator,
@ -20,7 +22,7 @@ pub struct App {
menu_bar: MenuBar,
viewport: Viewport,
workspace: Option<Workspace>,
workspace: Option<GuiWorkspace>,
}
impl Default for App {
@ -50,7 +52,7 @@ impl App {
fn update_state(&mut self) {
if let Ok(data) = self.content_channel.1.try_recv() {
self.workspace = Some(Workspace::new(
self.workspace = Some(GuiWorkspace::new(
Board::from_specctra(data.unwrap()),
&self.translator,
));

View File

@ -2,9 +2,9 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{viewport::Viewport, workspace::Workspace};
use topola::LayerId;
use crate::{viewport::Viewport, workspace::GuiWorkspace};
use topola::primitives::{Joint, Polygon, Segment, Via};
use topola::{LayerId, Workspace};
pub struct Display {}
@ -19,7 +19,7 @@ impl Display {
ui: &egui::Ui,
//menu_bar: &MenuBar,
viewport: &Viewport,
workspace: &Workspace,
workspace: &GuiWorkspace,
) {
self.display_layout(ctx, ui, /*menu_bar,*/ viewport, workspace);
self.display_bboxes(ctx, ui, viewport, workspace);
@ -33,9 +33,9 @@ impl Display {
ui: &egui::Ui,
//menu_bar: &MenuBar,
viewport: &Viewport,
workspace: &Workspace,
workspace: &GuiWorkspace,
) {
let board = workspace.autorouter.router().navmesher_board().board();
let board = workspace.workspace.board();
let layout = board.layout();
// Start from the bottom layer so that top layers are drawn on top.
@ -46,8 +46,10 @@ impl Display {
for joint_id in layout.layer_joints(layer) {
let joint = layout.joint(joint_id);
let pin_selected = board.pins_contain_joint(&workspace.selection.pins, joint_id);
let net_selected = board.nets_contain_joint(&workspace.selection.nets, joint_id);
let pin_selected =
board.pins_contain_joint(&workspace.workspace.selection().pins, joint_id);
let net_selected =
board.nets_contain_joint(&workspace.workspace.selection().nets, joint_id);
self.paint_joint(
ctx,
ui,
@ -64,9 +66,9 @@ impl Display {
for segment_id in layout.layer_segments(layer) {
let segment = layout.segment(segment_id);
let pin_selected =
board.pins_contain_segment(&workspace.selection.pins, segment_id);
board.pins_contain_segment(&workspace.workspace.selection().pins, segment_id);
let net_selected =
board.nets_contain_segment(&workspace.selection.nets, segment_id);
board.nets_contain_segment(&workspace.workspace.selection().nets, segment_id);
self.paint_segment(
ctx,
ui,
@ -82,8 +84,10 @@ impl Display {
for via_id in layout.layer_vias(layer) {
let via = layout.via(via_id);
let pin_selected = board.pins_contain_via(&workspace.selection.pins, via_id);
let net_selected = board.nets_contain_via(&workspace.selection.nets, via_id);
let pin_selected =
board.pins_contain_via(&workspace.workspace.selection().pins, via_id);
let net_selected =
board.nets_contain_via(&workspace.workspace.selection().nets, via_id);
self.paint_via(
ctx,
ui,
@ -100,9 +104,9 @@ impl Display {
for polygon_id in layout.layer_polygons(layer) {
let polygon = layout.polygon(polygon_id);
let pin_selected =
board.pins_contain_polygon(&workspace.selection.pins, polygon_id);
board.pins_contain_polygon(&workspace.workspace.selection().pins, polygon_id);
let net_selected =
board.nets_contain_polygon(&workspace.selection.nets, polygon_id);
board.nets_contain_polygon(&workspace.workspace.selection().nets, polygon_id);
self.paint_polygon(
ctx,
ui,
@ -203,9 +207,9 @@ impl Display {
ctx: &egui::Context,
ui: &egui::Ui,
viewport: &Viewport,
workspace: &Workspace,
workspace: &GuiWorkspace,
) {
let board = workspace.autorouter.router().navmesher_board().board();
let board = workspace.workspace.board();
let layout = board.layout();
for layer in (0..*layout.layer_count()).rev().map(LayerId::new) {
@ -283,20 +287,16 @@ impl Display {
ctx: &egui::Context,
ui: &egui::Ui,
viewport: &Viewport,
workspace: &Workspace,
workspace: &GuiWorkspace,
) {
for layer in (0..*workspace
.autorouter
.router()
.navmesher_board()
.board()
.layout()
.layer_count())
.map(LayerId::new)
{
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
return;
};
let autorouter = &autorouter_workspace.autorouter;
for layer in (0..*workspace.workspace.board().layout().layer_count()).map(LayerId::new) {
if workspace.appearance_panel.visible[layer.index()] {
for navmesh in workspace
.autorouter
for navmesh in autorouter
.router()
.navmesher_board()
.navmesher()
@ -354,9 +354,14 @@ impl Display {
_ctx: &egui::Context,
ui: &egui::Ui,
_viewport: &Viewport,
workspace: &Workspace,
workspace: &GuiWorkspace,
) {
for ratline in workspace.autorouter.ratsnest().ratlines() {
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
return;
};
let autorouter = &autorouter_workspace.autorouter;
for ratline in autorouter.ratsnest().ratlines() {
let layers = *ratline.endpoint_layers();
let endpoints = *ratline.endpoints();

View File

@ -3,15 +3,15 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use egui::Pos2;
use topola::{InteractiveInput, SelectionCombineMode, SelectionInteractor, Vector2};
use topola::{InteractiveInput, MasterInteractor, Vector2, Workspace};
use crate::{display::Display, workspace::Workspace};
use crate::{display::Display, workspace::GuiWorkspace};
pub struct Viewport {
pub scene_rect: egui::Rect,
pub ref_scene_rect: egui::Rect,
pub scheduled_zoom_to_fit: bool,
selection_interactor: Option<SelectionInteractor>,
master_interactor: Option<MasterInteractor>,
}
impl Viewport {
@ -20,11 +20,11 @@ impl Viewport {
scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)),
ref_scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)),
scheduled_zoom_to_fit: false,
selection_interactor: None,
master_interactor: None,
}
}
pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut Workspace>) {
pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut GuiWorkspace>) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint();
@ -52,7 +52,7 @@ impl Viewport {
if let Some(workspace) = workspace {
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
self.selection_interactor = None;
self.master_interactor = None;
}
let primary_pressed =
@ -61,6 +61,7 @@ impl Viewport {
ctx.input(|i| i.pointer.button_down(egui::PointerButton::Primary));
let primary_released =
ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary));
let delete_pressed = ctx.input(|i| i.key_pressed(egui::Key::Delete));
let mut maybe_pointer_on_scene: Option<Vector2<i64>> = None;
if let Some(pointer_viewport_pos) = ctx.input(|i| i.pointer.interact_pos()) {
@ -73,38 +74,101 @@ impl Viewport {
maybe_pointer_on_scene = Some(pointer_on_scene);
if primary_pressed && response.hovered() {
self.selection_interactor = Some(SelectionInteractor::new(
pointer_on_scene,
workspace.selection.clone(),
SelectionCombineMode::Replace,
self.master_interactor = Some(MasterInteractor::new(
None,
workspace.workspace.selection().clone(),
));
}
if let Some(interactor) = self.selection_interactor.as_mut() {
if let Some(interactor) = self.master_interactor.as_mut() {
if primary_down {
let _ = interactor.update(
workspace.autorouter.router().navmesher_board().board(),
let board = match &mut workspace.workspace {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_) => panic!("expected board workspace"),
};
interactor.update(
board,
workspace.appearance_panel.active,
InteractiveInput::new(pointer_on_scene, false, false),
InteractiveInput::new(pointer_on_scene, false, false, false),
);
if let Some(selection_interactor) =
interactor.selection_interactor().as_ref()
{
let origin = *selection_interactor.origin();
let drag_rect_scene = egui::Rect::from_min_max(
egui::pos2(
origin.x.min(pointer_on_scene.x) as f32,
origin.y.min(pointer_on_scene.y) as f32,
),
egui::pos2(
origin.x.max(pointer_on_scene.x) as f32,
origin.y.max(pointer_on_scene.y) as f32,
),
);
let drag_rect_on_viewport = egui::Rect::from_min_max(
scene_to_viewport * drag_rect_scene.min,
scene_to_viewport * drag_rect_scene.max,
);
let boundary_color = if pointer_on_scene.x >= origin.x {
egui::Color32::YELLOW
} else {
egui::Color32::from_rgb(80, 160, 255)
};
ui.painter().rect(
drag_rect_on_viewport,
egui::CornerRadius::ZERO,
egui::Color32::from_rgba_unmultiplied(80, 160, 255, 48),
egui::Stroke::new(1.5, boundary_color),
egui::StrokeKind::Outside,
);
}
}
}
}
if primary_released {
if let Some(mut interactor) = self.selection_interactor.take() {
let pointer_for_scene =
maybe_pointer_on_scene.unwrap_or(*interactor.origin());
let _ = interactor.update(
workspace.autorouter.router().navmesher_board().board(),
if let Some(mut interactor) = self.master_interactor.take() {
let pointer_for_scene = maybe_pointer_on_scene.unwrap_or_else(|| {
interactor
.selection_interactor()
.as_ref()
.map(|selection_interactor| *selection_interactor.origin())
.unwrap_or(Vector2::new(0, 0))
});
let board = match &mut workspace.workspace {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_) => panic!("expected board workspace"),
};
interactor.update(
board,
workspace.appearance_panel.active,
InteractiveInput::new(pointer_for_scene, true, false),
InteractiveInput::new(pointer_for_scene, true, false, false),
);
workspace.selection = interactor.selection().clone();
*workspace.workspace.selection_mut() = interactor.selection().clone();
}
}
if delete_pressed {
let pointer_for_scene =
maybe_pointer_on_scene.unwrap_or(Vector2::new(0, 0));
let mut interactor =
MasterInteractor::new(None, workspace.workspace.selection().clone());
let board = match &mut workspace.workspace {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_) => panic!("expected board workspace"),
};
interactor.update(
board,
workspace.appearance_panel.active,
InteractiveInput::new(pointer_for_scene, false, true, false),
);
*workspace.workspace.selection_mut() = interactor.selection().clone();
}
self.zoom_to_fit_if_scheduled(workspace);
}
})
@ -141,7 +205,7 @@ impl Viewport {
* egui::emath::TSTransform::from_scaling(scale)
}
fn zoom_to_fit_if_scheduled(&mut self, workspace: &Workspace) {
fn zoom_to_fit_if_scheduled(&mut self, workspace: &GuiWorkspace) {
if self.scheduled_zoom_to_fit {
self.scene_rect = Self::boundary_bounding_box(workspace);
self.ref_scene_rect = self.scene_rect.clone();
@ -150,29 +214,15 @@ impl Viewport {
self.scheduled_zoom_to_fit = false;
}
fn boundary_bounding_box(workspace: &Workspace) -> egui::Rect {
let first = workspace
.autorouter
.router()
.navmesher_board()
.board()
.layout()
.boundary()[0];
fn boundary_bounding_box(workspace: &GuiWorkspace) -> egui::Rect {
let first = workspace.workspace.board().layout().boundary()[0];
let mut min_x = first[0];
let mut max_x = first[0];
let mut min_y = first[1];
let mut max_y = first[1];
for point in workspace
.autorouter
.router()
.navmesher_board()
.board()
.layout()
.boundary()[1..]
.iter()
{
for point in workspace.workspace.board().layout().boundary()[1..].iter() {
if point[0] < min_x {
min_x = point[0];
}

View File

@ -2,29 +2,30 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use topola::{Autorouter, Board, selections::PersistableSelection};
use topola::{Board, Workspace};
use crate::{layers_panel::LayersPanel, translator::Translator};
pub struct Workspace {
pub autorouter: Autorouter,
pub struct GuiWorkspace {
pub workspace: Workspace,
pub appearance_panel: LayersPanel,
pub selection: PersistableSelection,
}
impl Workspace {
impl GuiWorkspace {
pub fn new(board: Board, tr: &Translator) -> Self {
let appearance_panel = LayersPanel::new(&board);
Self {
autorouter: Autorouter::new(board),
workspace: Workspace::new_board(board),
appearance_panel,
selection: PersistableSelection::new(),
}
}
pub fn update_appearance_panel(&mut self, ctx: &egui::Context) {
self.appearance_panel
.update(ctx, &self.autorouter.router().navmesher_board().board());
let Self {
workspace,
appearance_panel,
} = self;
appearance_panel.update(ctx, workspace.board());
}
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{
board::Board,
primitives::{JointId, PolygonId, SegmentId, ViaId},
};
impl Board {
pub fn delete_joint(&mut self, joint_id: JointId) {
self.layout.delete_joint(joint_id);
}
pub fn delete_segment(&mut self, segment_id: SegmentId) {
self.layout.delete_segment(segment_id);
}
pub fn delete_via(&mut self, via_id: ViaId) {
self.layout.delete_via(via_id);
}
pub fn delete_polygon(&mut self, polygon_id: PolygonId) {
self.layout.delete_polygon(polygon_id);
}
}

View File

@ -13,17 +13,19 @@ use crate::{
interactors::{InteractiveInput, SelectionCombineMode, SelectionContainMode},
selections::PersistableSelection,
},
layout::LayerId,
};
#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[derive(Clone, Constructor, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DragSelectionOptions {
combine: SelectionCombineMode,
contain: SelectionContainMode,
}
#[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct DragSelectionInteractor {
origin: Vector2<i64>,
layer: LayerId,
original_selection: PersistableSelection,
selection: PersistableSelection,
options: DragSelectionOptions,
@ -32,68 +34,64 @@ pub struct DragSelectionInteractor {
impl DragSelectionInteractor {
pub fn new(
origin: Vector2<i64>,
layer: LayerId,
original_selection: PersistableSelection,
options: DragSelectionOptions,
) -> Self {
Self {
origin,
layer,
original_selection,
selection: PersistableSelection::new(),
options,
}
}
pub fn update(
&mut self,
board: &Board,
input: InteractiveInput,
) -> Option<PersistableSelection> {
pub fn update(&mut self, board: &Board, input: InteractiveInput) {
if input.cancel {
self.selection = self.original_selection.clone();
return Some(self.selection.clone());
return;
}
self.selection = PersistableSelection::new();
for layer_index in 0..*board.layout().layer_count() {
let rect = Rect3::new(
Vector3::new(self.origin.x, self.origin.y, layer_index as i64),
Vector3::new(input.pointer.x, input.pointer.y, layer_index as i64),
);
let rect = Rect3::new(
Vector3::new(self.origin.x, self.origin.y, self.layer.index() as i64),
Vector3::new(input.pointer.x, input.pointer.y, self.layer.index() as i64),
);
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.components
.add(board.locate_components_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.components
.add(board.locate_components_inside_rect(rect));
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.components
.add(board.locate_components_intersecting_rect(rect));
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.nets
.add(board.locate_nets_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection.nets.add(board.locate_nets_inside_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.components
.add(board.locate_components_inside_rect(rect));
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.pins
.add(board.locate_pins_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection.pins.add(board.locate_pins_inside_rect(rect));
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.nets
.add(board.locate_nets_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection.nets.add(board.locate_nets_inside_rect(rect));
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.pins
.add(board.locate_pins_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection.pins.add(board.locate_pins_inside_rect(rect));
}
}
@ -141,6 +139,5 @@ impl DragSelectionInteractor {
}
self.selection = combined_selection;
Some(self.selection.clone())
}
}

View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::Constructor;
use crate::{
InteractiveInput,
board::{
Board,
interactors::{SelectionCombineMode, SelectionInteractor},
selections::PersistableSelection,
},
layout::LayerId,
};
#[derive(Clone, Constructor, Debug, Eq, Getters, PartialEq)]
pub struct MasterInteractor {
selection_interactor: Option<SelectionInteractor>,
selection: PersistableSelection,
}
impl MasterInteractor {
pub fn update(&mut self, board: &mut Board, layer: LayerId, input: InteractiveInput) {
if input.delete {
board.delete_net_free_primitives(self.selection.nets.clone());
}
if self.selection_interactor.is_none() {
self.selection_interactor = Some(SelectionInteractor::new(
input.pointer,
self.selection.clone(),
SelectionCombineMode::Replace,
));
}
if let Some(selection_interactor) = self.selection_interactor.as_mut() {
selection_interactor.update(board, layer, input.clone());
self.selection = selection_interactor.selection().clone();
}
if input.release || input.cancel {
self.selection_interactor = None;
}
}
}

View File

@ -3,31 +3,25 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
mod drag_selection;
mod master;
mod selection;
use derive_more::Constructor;
pub use drag_selection::{DragSelectionInteractor, DragSelectionOptions};
pub use master::MasterInteractor;
pub use selection::SelectionInteractor;
use serde::{Deserialize, Serialize};
use crate::Vector2;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct InteractiveInput {
pointer: Vector2<i64>,
released: bool,
release: bool,
delete: bool,
cancel: bool,
}
impl InteractiveInput {
pub fn new(pointer: Vector2<i64>, released: bool, cancel: bool) -> Self {
Self {
pointer,
released,
cancel,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum SelectionCombineMode {
Replace,

View File

@ -17,7 +17,7 @@ use crate::{
layout::LayerId,
};
#[derive(Clone, Debug, Eq, Getters, Ord, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct SelectionInteractor {
origin: Vector2<i64>,
original_selection: PersistableSelection,
@ -39,18 +39,13 @@ impl SelectionInteractor {
}
}
pub fn update(
&mut self,
board: &Board,
layer: LayerId,
input: InteractiveInput,
) -> Option<PersistableSelection> {
pub fn update(&mut self, board: &Board, layer: LayerId, input: InteractiveInput) {
if input.cancel {
self.selection = self.original_selection.clone();
return Some(self.selection.clone());
return;
}
if input.released && input.pointer == self.origin {
if input.release && input.pointer == self.origin {
let mut selection = self.original_selection.clone();
// Pins have intentional precedence over nets and components.
@ -76,8 +71,8 @@ impl SelectionInteractor {
.xor(std::iter::once(component_selector));
}
self.selection = selection.clone();
return Some(selection);
self.selection = selection;
return;
}
let contain = if input.pointer.x >= self.origin.x {
@ -87,11 +82,14 @@ impl SelectionInteractor {
};
let options = DragSelectionOptions::new(self.combine.clone(), contain);
let mut drag_selection_interactor =
DragSelectionInteractor::new(self.origin, self.original_selection.clone(), options);
let selection = drag_selection_interactor.update(board, input)?;
let mut drag_selection_interactor = DragSelectionInteractor::new(
self.origin,
layer,
self.original_selection.clone(),
options,
);
self.selection = selection.clone();
Some(selection)
drag_selection_interactor.update(board, input);
self.selection = drag_selection_interactor.selection().clone();
}
}

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod delete;
mod insert;
pub mod interactors;
mod layer;
@ -23,7 +22,7 @@ use crate::{
LayerId, Layout, LayoutHalfDelta,
compounds::{ComponentId, NetId, PinId},
},
math::Vector2,
vector::Vector2,
};
#[derive(Clone, Debug, Getters, Delta)]

View File

@ -5,15 +5,91 @@
use crate::{
board::{Board, selections::ComponentSelection},
layout::compounds::ComponentId,
primitives::{JointId, PolygonId, SegmentId, ViaId},
selections::NetSelection,
};
impl Board {
pub fn resolve_components(&self, selection: ComponentSelection) -> Vec<ComponentId> {
pub fn resolve_components(
&self,
selection: ComponentSelection,
) -> impl Iterator<Item = ComponentId> {
selection
.0
.clone()
.into_iter()
.filter_map(|selector| self.component_id(&selector.component))
.collect()
}
pub fn resolve_net_joints(&self, selection: NetSelection) -> impl Iterator<Item = JointId> {
let mut resolved_joints = Vec::new();
for (index, _) in self.layout.joints().container() {
let joint_id = JointId::new(index);
let Some(selector) = self.joint_net_selector(joint_id) else {
continue;
};
if selection.0.contains(&selector) {
resolved_joints.push(joint_id);
}
}
resolved_joints.into_iter()
}
pub fn resolve_net_segments(&self, selection: NetSelection) -> impl Iterator<Item = SegmentId> {
let mut resolved_segments = Vec::new();
for (index, _) in self.layout.segments().container() {
let segment_id = SegmentId::new(index);
let Some(selector) = self.segment_net_selector(segment_id) else {
continue;
};
if selection.0.contains(&selector) {
resolved_segments.push(segment_id);
}
}
resolved_segments.into_iter()
}
pub fn resolve_net_vias(&self, selection: NetSelection) -> impl Iterator<Item = ViaId> {
let mut resolved_vias = Vec::new();
for (index, _) in self.layout.vias().container() {
let via_id = ViaId::new(index);
let Some(selector) = self.via_net_selector(via_id) else {
continue;
};
if selection.0.contains(&selector) {
resolved_vias.push(via_id);
}
}
resolved_vias.into_iter()
}
pub fn resolve_net_polygons(&self, selection: NetSelection) -> impl Iterator<Item = PolygonId> {
let mut resolved_polygons = Vec::new();
for (index, _) in self.layout.polygons().container() {
let polygon_id = PolygonId::new(index);
let Some(selector) = self.polygon_net_selector(polygon_id) else {
continue;
};
if selection.0.contains(&selector) {
resolved_polygons.push(polygon_id);
}
}
resolved_polygons.into_iter()
}
}

View File

@ -4,7 +4,7 @@
use std::collections::BTreeSet;
use derive_more::Constructor;
use derive_more::{Constructor, IntoIterator};
use serde::{Deserialize, Serialize};
#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
@ -12,7 +12,9 @@ pub struct ComponentSelector {
pub component: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[derive(
Clone, Debug, Default, Deserialize, Eq, IntoIterator, Ord, PartialEq, PartialOrd, Serialize,
)]
pub struct ComponentSelection(pub BTreeSet<ComponentSelector>);
impl ComponentSelection {

View File

@ -4,6 +4,7 @@
use std::collections::BTreeSet;
use derive_more::IntoIterator;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
@ -17,7 +18,9 @@ impl NetSelector {
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[derive(
Clone, Debug, Default, Deserialize, Eq, IntoIterator, Ord, PartialEq, PartialOrd, Serialize,
)]
pub struct NetSelection(pub BTreeSet<NetSelector>);
impl NetSelection {

View File

@ -4,7 +4,7 @@
use std::collections::BTreeSet;
use derive_more::Constructor;
use derive_more::{Constructor, IntoIterator};
use serde::{Deserialize, Serialize};
#[derive(
@ -15,7 +15,9 @@ pub struct PinSelector {
pub layer: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[derive(
Clone, Debug, Default, Deserialize, Eq, IntoIterator, Ord, PartialEq, PartialOrd, Serialize,
)]
pub struct PinSelection(pub BTreeSet<PinSelector>);
impl PinSelection {

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{Board, selections::NetSelection};
impl Board {
pub fn delete_net_free_primitives(&mut self, selection: NetSelection) {
for joint_id in self
.resolve_net_joints(selection.clone())
.filter(|&joint_id| self.layout.joint(joint_id).spec.pin.is_none())
.collect::<Vec<_>>()
.clone()
{
self.layout.delete_joint(joint_id);
}
for segment_id in self
.resolve_net_segments(selection.clone())
.filter(|&segment_id| self.layout.segment(segment_id).spec.pin.is_none())
.collect::<Vec<_>>()
.clone()
{
self.layout.delete_segment(segment_id);
}
for via_id in self
.resolve_net_vias(selection.clone())
.filter(|&via_id| self.layout.via(via_id).spec.pin.is_none())
.collect::<Vec<_>>()
.clone()
{
self.layout.delete_via(via_id);
}
for polygon_id in self
.resolve_net_polygons(selection.clone())
.filter(|&polygon_id| self.layout.polygon(polygon_id).pin.is_none())
.collect::<Vec<_>>()
.clone()
{
self.layout.delete_polygon(polygon_id);
}
}
}

View File

@ -2,4 +2,5 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod delete;
mod move_by;

View File

@ -5,12 +5,11 @@
use crate::{Board, Vector2, layout::compounds::ComponentId, selections::ComponentSelection};
impl Board {
pub fn move_components_by(
&mut self,
selection: &ComponentSelection,
translation: Vector2<i64>,
) {
self.move_resolved_components_by(&self.resolve_components(selection.clone()), translation);
pub fn move_components_by(&mut self, selection: ComponentSelection, translation: Vector2<i64>) {
self.move_resolved_components_by(
&self.resolve_components(selection).collect::<Vec<_>>(),
translation,
);
}
pub fn move_resolved_components_by(

View File

@ -24,73 +24,94 @@ impl Layout {
&self,
rect: Rect3<i64>,
) -> impl Iterator<Item = JointId> {
let rect_aabb = rect.aabb3();
self.joints_rtree
.as_ref()
.locate_in_envelope_intersecting(&rect_aabb)
.locate_in_envelope_intersecting(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&joint_id| {
let joint = self.joint(joint_id);
rect.rect2()
.intersects_circle(joint.spec.position, joint.spec.radius as i64)
})
}
pub fn locate_joints_inside_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = JointId> {
let rect_aabb = rect.aabb3();
self.joints_rtree
.as_ref()
.locate_in_envelope(&rect_aabb)
.locate_in_envelope(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&joint_id| {
let joint = self.joint(joint_id);
rect.rect2()
.contains_circle(joint.spec.position, joint.spec.radius as i64)
})
}
pub fn locate_segments_at_point(&self, point: Vector3<i64>) -> impl Iterator<Item = SegmentId> {
let point2 = point.xy();
self.segments_rtree
.as_ref()
.locate_all_at_point(&[point.x, point.y, point.z])
.map(|geom_with_data| geom_with_data.data)
.filter(move |&segment_id| self.segment(segment_id).contains_point(point2))
.filter(move |&segment_id| self.segment(segment_id).contains_point(point.xy()))
}
pub fn locate_segments_intersecting_rect(
&self,
rect: Rect3<i64>,
) -> impl Iterator<Item = SegmentId> {
let rect_aabb = rect.aabb3();
self.segments_rtree
.as_ref()
.locate_in_envelope_intersecting(&rect_aabb)
.locate_in_envelope_intersecting(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&segment_id| {
let segment = self.segment(segment_id);
rect.rect2()
.intersects_polygon(&segment.bounding_rectangle())
})
}
pub fn locate_segments_inside_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = SegmentId> {
let rect_aabb = rect.aabb3();
self.segments_rtree
.as_ref()
.locate_in_envelope(&rect_aabb)
.locate_in_envelope(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&segment_id| {
let segment = self.segment(segment_id);
rect.rect2().contains_polygon(&segment.bounding_rectangle())
})
}
pub fn locate_vias_at_point(&self, point: Vector3<i64>) -> impl Iterator<Item = ViaId> {
let layer = LayerId::new(point.z as usize);
let point2 = point.xy();
self.vias_rtree
.as_ref()
.locate_all_at_point(&[point.x, point.y, point.z])
.map(|geom_with_data| geom_with_data.data)
.filter(move |&via_id| self.vias[via_id.index()].contains_point(layer, point2))
.filter(move |&via_id| self.vias[via_id.index()].contains_point(layer, point.xy()))
}
pub fn locate_vias_intersecting_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = ViaId> {
let rect_aabb = rect.aabb3();
self.vias_rtree
.as_ref()
.locate_in_envelope_intersecting(&rect_aabb)
.locate_in_envelope_intersecting(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&via_id| {
let via = self.via(via_id);
rect.rect2()
.intersects_circle(via.position, via.spec.radius as i64)
})
}
pub fn locate_vias_inside_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = ViaId> {
let rect_aabb = rect.aabb3();
self.vias_rtree
.as_ref()
.locate_in_envelope(&rect_aabb)
.locate_in_envelope(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&via_id| {
let via = self.via(via_id);
rect.rect2()
.contains_circle(via.position, via.spec.radius as i64)
})
}
pub fn locate_polygons_at_point(&self, point: Vector3<i64>) -> impl Iterator<Item = PolygonId> {
@ -106,19 +127,25 @@ impl Layout {
&self,
rect: Rect3<i64>,
) -> impl Iterator<Item = PolygonId> {
let rect_aabb = rect.aabb3();
self.polygons_rtree
.as_ref()
.locate_in_envelope_intersecting(&rect_aabb)
.locate_in_envelope_intersecting(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&polygon_id| {
let polygon = self.polygon(polygon_id);
rect.rect2().intersects_polygon(&polygon.vertices)
})
}
pub fn locate_polygons_inside_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = PolygonId> {
let rect_aabb = rect.aabb3();
self.polygons_rtree
.as_ref()
.locate_in_envelope(&rect_aabb)
.locate_in_envelope(&rect.aabb3())
.map(|geom_with_data| geom_with_data.data)
.filter(move |&polygon_id| {
let polygon = self.polygon(polygon_id);
rect.rect2().contains_polygon(&polygon.vertices)
})
}
pub fn locate_nets_intersecting_rect(&self, rect: Rect3<i64>) -> impl Iterator<Item = NetId> {

View File

@ -8,9 +8,8 @@ use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::math::Vector2;
use super::{SegmentId, ViaId};
use crate::primitives::{SegmentId, ViaId};
use crate::vector::Vector2;
#[derive(
Clone,

View File

@ -18,5 +18,6 @@ pub use via::*;
pub enum PrimitiveId {
Joint(JointId),
Segment(SegmentId),
Via(ViaId),
Polygon(PolygonId),
}

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::math::Vector2;
use crate::vector::Vector2;
#[derive(
Clone,

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::math::Vector2;
use crate::vector::Vector2;
use super::JointId;

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::math::Vector2;
use crate::vector::Vector2;
use super::JointId;

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{Layout, layout::compounds::ComponentId, math::Vector2};
use crate::{Layout, layout::compounds::ComponentId, vector::Vector2};
impl Layout {
pub fn move_component_by(&mut self, id: ComponentId, translation: Vector2<i64>) {

View File

@ -10,8 +10,11 @@ mod math;
mod navmesher;
mod pathfinder;
mod ratsnest;
mod rect;
mod router;
mod specctra;
mod vector;
mod workspace;
pub use crate::autorouter::Autorouter;
pub use crate::board::Board;
@ -19,13 +22,15 @@ pub use crate::board::LayerDesc;
pub use crate::board::LayerSide;
pub use crate::board::LayerType;
pub use crate::board::interactors::{
DragSelectionInteractor, DragSelectionOptions, InteractiveInput, SelectionCombineMode,
SelectionContainMode, SelectionInteractor,
DragSelectionInteractor, DragSelectionOptions, InteractiveInput, MasterInteractor,
SelectionCombineMode, SelectionContainMode, SelectionInteractor,
};
pub use crate::board::selections;
pub use crate::layout::LayerId;
pub use crate::layout::Layout;
pub use crate::layout::compounds::{Pin, PinId};
pub use crate::layout::primitives;
pub use crate::math::{Rect2, Rect3, Vector2, Vector3};
pub use crate::ratsnest::{Ratline, Ratsnest};
pub use crate::rect::{Rect2, Rect3};
pub use crate::vector::{Vector2, Vector3};
pub use crate::workspace::{AutorouterWorkspace, BoardWorkspace, Workspace};

View File

@ -2,208 +2,9 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::{
Add, AddAssign, Constructor, Div, DivAssign, From, Into, Mul, MulAssign, Sub, SubAssign,
};
use polygon_unionfind::UnionFind;
use rstar::AABB;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Getters, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Rect2<T> {
min: Vector2<T>,
max: Vector2<T>,
}
impl<T: Ord + Copy> Rect2<T> {
pub fn new(from: Vector2<T>, to: Vector2<T>) -> Self {
Self {
min: Vector2::new(std::cmp::min(from.x, to.x), std::cmp::min(from.y, to.y)),
max: Vector2::new(std::cmp::max(from.x, to.x), std::cmp::max(from.y, to.y)),
}
}
}
impl Rect2<i64> {
pub fn aabb3(self, z: i64) -> AABB<[i64; 3]> {
AABB::from_corners([self.min.x, self.min.y, z], [self.max.x, self.max.y, z])
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Getters, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Rect3<T> {
min: Vector3<T>,
max: Vector3<T>,
}
impl<T: Ord + Copy> Rect3<T> {
pub fn new(from: Vector3<T>, to: Vector3<T>) -> Self {
Self {
min: Vector3::new(
std::cmp::min(from.x, to.x),
std::cmp::min(from.y, to.y),
std::cmp::min(from.z, to.z),
),
max: Vector3::new(
std::cmp::max(from.x, to.x),
std::cmp::max(from.y, to.y),
std::cmp::max(from.z, to.z),
),
}
}
}
impl Rect3<i64> {
pub fn aabb3(self) -> AABB<[i64; 3]> {
AABB::from_corners(
[self.min.x, self.min.y, self.min.z],
[self.max.x, self.max.y, self.max.z],
)
}
}
#[derive(
Add,
AddAssign,
Clone,
Constructor,
Copy,
Debug,
Deserialize,
Div,
DivAssign,
Eq,
From,
Into,
Mul,
MulAssign,
Ord,
PartialEq,
PartialOrd,
Serialize,
Sub,
SubAssign,
)]
pub struct Vector2<T> {
pub x: T,
pub y: T,
}
#[derive(
Add,
AddAssign,
Clone,
Constructor,
Copy,
Debug,
Deserialize,
Div,
DivAssign,
Eq,
From,
Into,
Mul,
MulAssign,
Ord,
PartialEq,
PartialOrd,
Serialize,
Sub,
SubAssign,
)]
pub struct Vector3<T> {
pub x: T,
pub y: T,
pub z: T,
}
impl<T: Copy> Vector3<T> {
pub fn xy(self) -> Vector2<T> {
Vector2::new(self.x, self.y)
}
}
impl<T: Copy> From<[T; 2]> for Vector2<T> {
fn from(from: [T; 2]) -> Self {
Self {
x: from[0],
y: from[1],
}
}
}
impl<T: Copy> From<Vector2<T>> for [T; 2] {
fn from(from: Vector2<T>) -> Self {
[from.x, from.y]
}
}
macro_rules! impl_inside_polygon {
($type:ty) => {
impl Vector2<$type> {
// Checks if the point is inside a polygon by casting a ray to the
// right. Division is not used to avoid integer truncation errors.
pub fn inside_polygon(&self, polygon: &[Vector2<$type>]) -> bool {
let mut inside = false;
let n = polygon.len();
// `self` is `v0`.
// `v1` is the previous vertex.
let mut v1 = &polygon[n - 1];
// `v2` is the current vertex.
for v2 in polygon.iter() {
let dx12 = v2.x - v1.x;
let dy12 = v2.y - v1.y;
// First, check if the line of the horizontal rightward ray
// cast to actually crosses the vertical span of the current
// `(v1, v2)` edge.
if dy12 != (0 as $type) && (self.y > v1.y) != (self.y > v2.y) {
let dx01 = self.x - v1.x;
let dy01 = self.y - v1.y;
// Now check if the (v1, v2) edge is actually on the
// right side of the ray and not on the left.
//
// This just compares the X coordinate of `self` (`v0`)
// to the X coordinate of the intersection between the
// horizontal rightward ray and the current `(v1, v2)` edge:
//
// `self.x < v1.x + (self.y - v1.y) * (dx12 / dy12)`
//
// but is algebraically simplified and rewritten to not
// use division.
let crosses = if dy12 > (0 as $type) {
dx01 * dy12 < dx12 * dy01
} else {
dx01 * dy12 > dx12 * dy01
};
// Even-odd rule: flip whether the point is inside or
// outside upon each detected crossing.
if crosses {
inside = !inside;
}
}
// Make the current vertex previous for the next loop
// iteration.
v1 = v2;
}
inside
}
}
};
}
impl_inside_polygon!(f32);
impl_inside_polygon!(f64);
impl_inside_polygon!(i32);
impl_inside_polygon!(i64);
use crate::Vector2;
/// Returns the four vertices of a segment inflated by `half_width`, forming a
/// convex quadrilateral. The segment goes from (x1, y1) to (x2, y2).
@ -225,60 +26,6 @@ pub fn inflated_segment(x1: i64, y1: i64, x2: i64, y2: i64, half_width: u64) ->
]
}
macro_rules! impl_rotate_around_point {
($type:ty) => {
impl Vector2<$type> {
pub fn rotate_around_point(&mut self, angle: $type, origin: Vector2<$type>) -> Self {
let sin = angle.sin();
let cos = angle.cos();
let tx = self.x - origin.x;
let ty = self.y - origin.y;
let rx = tx * cos - ty * sin;
let ry = tx * sin + ty * cos;
self.x = rx + origin.x;
self.y = ry + origin.y;
*self
}
pub fn rotate_around_point_degrees(
&mut self,
angle: $type,
origin: Vector2<$type>,
) -> Self {
self.rotate_around_point(angle.to_radians(), origin)
}
}
};
}
impl_rotate_around_point!(f32);
impl_rotate_around_point!(f64);
macro_rules! impl_polygon_centroid {
($type:ty) => {
impl Vector2<$type> {
pub fn polygon_centroid(polygon: &[Vector2<$type>]) -> Self {
let mut sum = Vector2::new(0 as $type, 0 as $type);
for vertex in polygon.iter() {
sum += *vertex;
}
sum / polygon.len() as $type
}
}
};
}
impl_polygon_centroid!(f32);
impl_polygon_centroid!(f64);
impl_polygon_centroid!(i32);
impl_polygon_centroid!(i64);
/// Kruskal's minimum spanning tree algorithm.
pub fn kruskal_mst<W: Copy + Ord>(
vertex_count: usize,

View File

@ -12,8 +12,8 @@ use undoredo::Recorder;
use crate::{
Board,
layout::LayerId,
math::Vector2,
primitives::{Joint, JointId, JointSpec, Polygon, PolygonId, Segment, SegmentId},
vector::Vector2,
};
#[derive(

View File

@ -12,8 +12,8 @@ use crate::{
Board,
layout::LayerId,
layout::compounds::NetId,
math::Vector2,
primitives::{JointId, PolygonId, PrimitiveId, SegmentId},
vector::Vector2,
};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Getters, Ord, PartialEq, PartialOrd, Serialize)]

209
topola/src/rect.rs Normal file
View File

@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use rstar::{AABB, RTreeNum};
use serde::{Deserialize, Serialize};
use crate::{Vector2, Vector3};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Getters, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Rect2<T> {
min: Vector2<T>,
max: Vector2<T>,
}
impl<T: Copy> Rect2<T> {
pub fn corners(&self) -> [Vector2<T>; 4] {
[
self.min,
Vector2::new(self.max.x, self.min.y),
self.max,
Vector2::new(self.min.x, self.max.y),
]
}
}
impl<T: Copy + Ord> Rect2<T> {
pub fn new(from: Vector2<T>, to: Vector2<T>) -> Self {
Self {
min: Vector2::new(std::cmp::min(from.x, to.x), std::cmp::min(from.y, to.y)),
max: Vector2::new(std::cmp::max(from.x, to.x), std::cmp::max(from.y, to.y)),
}
}
}
impl<T: RTreeNum> Rect2<T> {
pub fn aabb3(self, z: T) -> AABB<[T; 3]> {
AABB::from_corners([self.min.x, self.min.y, z], [self.max.x, self.max.y, z])
}
}
macro_rules! impl_rect2_contains_circle {
($type:ty) => {
impl Rect2<$type> {
pub fn contains_circle(&self, center: Vector2<$type>, radius: $type) -> bool {
let min_x = self.min.x as f64;
let min_y = self.min.y as f64;
let max_x = self.max.x as f64;
let max_y = self.max.y as f64;
let cx = center.x as f64;
let cy = center.y as f64;
let r = radius as f64;
cx - r >= min_x && cx + r <= max_x && cy - r >= min_y && cy + r <= max_y
}
}
};
}
impl_rect2_contains_circle!(i8);
impl_rect2_contains_circle!(i16);
impl_rect2_contains_circle!(i32);
impl_rect2_contains_circle!(i64);
impl_rect2_contains_circle!(i128);
impl_rect2_contains_circle!(u8);
impl_rect2_contains_circle!(u16);
impl_rect2_contains_circle!(u32);
impl_rect2_contains_circle!(u64);
impl_rect2_contains_circle!(u128);
impl_rect2_contains_circle!(f32);
impl_rect2_contains_circle!(f64);
macro_rules! impl_rect2_intersects_circle {
($type:ty) => {
impl Rect2<$type> {
pub fn intersects_circle(&self, center: Vector2<$type>, radius: $type) -> bool {
let min_x = self.min.x as f64;
let min_y = self.min.y as f64;
let max_x = self.max.x as f64;
let max_y = self.max.y as f64;
let cx = center.x as f64;
let cy = center.y as f64;
let r = radius as f64;
let closest_x = cx.clamp(min_x, max_x);
let closest_y = cy.clamp(min_y, max_y);
let dx = cx - closest_x;
let dy = cy - closest_y;
dx * dx + dy * dy <= r * r
}
}
};
}
impl_rect2_intersects_circle!(i8);
impl_rect2_intersects_circle!(i16);
impl_rect2_intersects_circle!(i32);
impl_rect2_intersects_circle!(i64);
impl_rect2_intersects_circle!(i128);
impl_rect2_intersects_circle!(u8);
impl_rect2_intersects_circle!(u16);
impl_rect2_intersects_circle!(u32);
impl_rect2_intersects_circle!(u64);
impl_rect2_intersects_circle!(u128);
impl_rect2_intersects_circle!(f32);
impl_rect2_intersects_circle!(f64);
macro_rules! impl_rect2_contains_polygon {
($type:ty) => {
impl Rect2<$type> {
pub fn contains_polygon(&self, polygon: &[Vector2<$type>]) -> bool {
let corners = self.corners();
polygon.iter().all(|point| point.inside_polygon(&corners))
}
}
};
}
impl_rect2_contains_polygon!(i8);
impl_rect2_contains_polygon!(i16);
impl_rect2_contains_polygon!(i32);
impl_rect2_contains_polygon!(i64);
impl_rect2_contains_polygon!(i128);
impl_rect2_contains_polygon!(u8);
impl_rect2_contains_polygon!(u16);
impl_rect2_contains_polygon!(u32);
impl_rect2_contains_polygon!(u64);
impl_rect2_contains_polygon!(u128);
impl_rect2_contains_polygon!(f32);
impl_rect2_contains_polygon!(f64);
macro_rules! impl_rect2_intersects_polygon {
($type:ty) => {
impl Rect2<$type> {
pub fn intersects_polygon(&self, polygon: &[Vector2<$type>]) -> bool {
if polygon.is_empty() {
return false;
}
if self.contains_polygon(polygon) {
return true;
}
let corners = self.corners();
if corners.iter().any(|corner| corner.inside_polygon(polygon)) {
return true;
}
false
// TODO: Now handle cases where no vertex is inside but only
// edges intersect.
}
}
};
}
impl_rect2_intersects_polygon!(i8);
impl_rect2_intersects_polygon!(i16);
impl_rect2_intersects_polygon!(i32);
impl_rect2_intersects_polygon!(i64);
impl_rect2_intersects_polygon!(i128);
impl_rect2_intersects_polygon!(u8);
impl_rect2_intersects_polygon!(u16);
impl_rect2_intersects_polygon!(u32);
impl_rect2_intersects_polygon!(u64);
impl_rect2_intersects_polygon!(u128);
impl_rect2_intersects_polygon!(f32);
impl_rect2_intersects_polygon!(f64);
#[derive(Clone, Copy, Debug, Deserialize, Eq, Getters, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Rect3<T> {
min: Vector3<T>,
max: Vector3<T>,
}
impl<T: Ord + Copy> Rect3<T> {
pub fn new(from: Vector3<T>, to: Vector3<T>) -> Self {
Self {
min: Vector3::new(
std::cmp::min(from.x, to.x),
std::cmp::min(from.y, to.y),
std::cmp::min(from.z, to.z),
),
max: Vector3::new(
std::cmp::max(from.x, to.x),
std::cmp::max(from.y, to.y),
std::cmp::max(from.z, to.z),
),
}
}
}
impl<T: Copy + Ord> Rect3<T> {
pub fn rect2(&self) -> Rect2<T> {
Rect2::new(self.min.xy(), self.max.xy())
}
}
impl<T: RTreeNum> Rect3<T> {
pub fn aabb3(&self) -> AABB<[T; 3]> {
AABB::from_corners(
[self.min.x, self.min.y, self.min.z],
[self.max.x, self.max.y, self.max.z],
)
}
}

View File

@ -14,8 +14,8 @@ use crate::{
board::{Board, LayerDesc, LayerSide, LayerType},
layout::LayerId,
layout::compounds::{ComponentId, NetId, PinId},
math::Vector2,
primitives::{JointSpec, Polygon, Segment, SegmentSpec},
vector::Vector2,
};
impl Board {

218
topola/src/vector.rs Normal file
View File

@ -0,0 +1,218 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_more::{
Add, AddAssign, Constructor, Div, DivAssign, From, Into, Mul, MulAssign, Sub, SubAssign,
};
use serde::{Deserialize, Serialize};
#[derive(
Add,
AddAssign,
Clone,
Constructor,
Copy,
Debug,
Deserialize,
Div,
DivAssign,
Eq,
From,
Into,
Mul,
MulAssign,
Ord,
PartialEq,
PartialOrd,
Serialize,
Sub,
SubAssign,
)]
pub struct Vector2<T> {
pub x: T,
pub y: T,
}
#[derive(
Add,
AddAssign,
Clone,
Constructor,
Copy,
Debug,
Deserialize,
Div,
DivAssign,
Eq,
From,
Into,
Mul,
MulAssign,
Ord,
PartialEq,
PartialOrd,
Serialize,
Sub,
SubAssign,
)]
pub struct Vector3<T> {
pub x: T,
pub y: T,
pub z: T,
}
impl<T: Copy> Vector3<T> {
pub fn xy(self) -> Vector2<T> {
Vector2::new(self.x, self.y)
}
}
impl<T: Copy> From<[T; 2]> for Vector2<T> {
fn from(from: [T; 2]) -> Self {
Self {
x: from[0],
y: from[1],
}
}
}
impl<T: Copy> From<Vector2<T>> for [T; 2] {
fn from(from: Vector2<T>) -> Self {
[from.x, from.y]
}
}
macro_rules! impl_vector2_inside_polygon {
($type:ty) => {
impl Vector2<$type> {
// Checks if the point is inside a polygon by casting a ray to the
// right. Division is not used to avoid integer truncation errors.
pub fn inside_polygon(&self, polygon: &[Vector2<$type>]) -> bool {
let mut inside = false;
let n = polygon.len();
// `self` is `v0`.
// `v1` is the previous vertex.
let mut v1 = &polygon[n - 1];
// `v2` is the current vertex.
for v2 in polygon.iter() {
let dx12 = v2.x - v1.x;
let dy12 = v2.y - v1.y;
// First, check if the line of the horizontal rightward ray
// cast to actually crosses the vertical span of the current
// `(v1, v2)` edge.
if dy12 != (0 as $type) && (self.y > v1.y) != (self.y > v2.y) {
let dx01 = self.x - v1.x;
let dy01 = self.y - v1.y;
// Now check if the (v1, v2) edge is actually on the
// right side of the ray and not on the left.
//
// This just compares the X coordinate of `self` (`v0`)
// to the X coordinate of the intersection between the
// horizontal rightward ray and the current `(v1, v2)` edge:
//
// `self.x < v1.x + (self.y - v1.y) * (dx12 / dy12)`
//
// but is algebraically simplified and rewritten to not
// use division.
let crosses = if dy12 > (0 as $type) {
dx01 * dy12 < dx12 * dy01
} else {
dx01 * dy12 > dx12 * dy01
};
// Even-odd rule: flip whether the point is inside or
// outside upon each detected crossing.
if crosses {
inside = !inside;
}
}
// Make the current vertex previous for the next loop
// iteration.
v1 = v2;
}
inside
}
}
};
}
impl_vector2_inside_polygon!(i8);
impl_vector2_inside_polygon!(i16);
impl_vector2_inside_polygon!(i32);
impl_vector2_inside_polygon!(i64);
impl_vector2_inside_polygon!(i128);
impl_vector2_inside_polygon!(u8);
impl_vector2_inside_polygon!(u16);
impl_vector2_inside_polygon!(u32);
impl_vector2_inside_polygon!(u64);
impl_vector2_inside_polygon!(u128);
impl_vector2_inside_polygon!(f32);
impl_vector2_inside_polygon!(f64);
macro_rules! impl_vector2_rotate_around_point {
($type:ty) => {
impl Vector2<$type> {
pub fn rotate_around_point(&mut self, angle: $type, origin: Vector2<$type>) -> Self {
let sin = angle.sin();
let cos = angle.cos();
let tx = self.x - origin.x;
let ty = self.y - origin.y;
let rx = tx * cos - ty * sin;
let ry = tx * sin + ty * cos;
self.x = rx + origin.x;
self.y = ry + origin.y;
*self
}
pub fn rotate_around_point_degrees(
&mut self,
angle: $type,
origin: Vector2<$type>,
) -> Self {
self.rotate_around_point(angle.to_radians(), origin)
}
}
};
}
impl_vector2_rotate_around_point!(f32);
impl_vector2_rotate_around_point!(f64);
macro_rules! impl_polygon_centroid {
($type:ty) => {
impl Vector2<$type> {
pub fn polygon_centroid(polygon: &[Vector2<$type>]) -> Self {
let mut sum = Vector2::new(0 as $type, 0 as $type);
for vertex in polygon.iter() {
sum += *vertex;
}
sum / polygon.len() as $type
}
}
};
}
impl_polygon_centroid!(i8);
impl_polygon_centroid!(i16);
impl_polygon_centroid!(i32);
impl_polygon_centroid!(i64);
impl_polygon_centroid!(u8);
impl_polygon_centroid!(u16);
impl_polygon_centroid!(u32);
impl_polygon_centroid!(u64);
impl_polygon_centroid!(f32);
impl_polygon_centroid!(f64);

71
topola/src/workspace.rs Normal file
View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{Autorouter, Board, selections::PersistableSelection};
pub enum Workspace {
Board(BoardWorkspace),
Autorouter(AutorouterWorkspace),
}
impl Workspace {
pub fn new_board(board: Board) -> Self {
Self::Board(BoardWorkspace::new(board))
}
pub fn new_autorouter(board: Board) -> Self {
Self::Autorouter(AutorouterWorkspace::new(board))
}
pub fn selection(&self) -> &PersistableSelection {
match self {
Workspace::Board(workspace) => &workspace.selection,
Workspace::Autorouter(workspace) => &workspace.selection,
}
}
pub fn selection_mut(&mut self) -> &mut PersistableSelection {
match self {
Workspace::Board(workspace) => &mut workspace.selection,
Workspace::Autorouter(workspace) => &mut workspace.selection,
}
}
pub fn board(&self) -> &Board {
match self {
Workspace::Board(workspace) => &workspace.board,
Workspace::Autorouter(workspace) => {
workspace.autorouter.router().navmesher_board().board()
}
}
}
}
pub struct BoardWorkspace {
pub board: Board,
pub selection: PersistableSelection,
}
impl BoardWorkspace {
pub fn new(board: Board) -> Self {
Self {
board,
selection: PersistableSelection::new(),
}
}
}
pub struct AutorouterWorkspace {
pub autorouter: Autorouter,
pub selection: PersistableSelection,
}
impl AutorouterWorkspace {
pub fn new(board: Board) -> Self {
Self {
autorouter: Autorouter::new(board),
selection: PersistableSelection::new(),
}
}
}