Compare commits

...

5 Commits

Author SHA1 Message Date
Mikolaj Wielgus 49cbf8b314 Add compass directions 2026-06-01 13:45:03 +02:00
Mikolaj Wielgus 1264c3183a Add capability to move components by dragging mouse 2026-05-29 16:09:30 +02:00
Mikolaj Wielgus 77950109fc Add interactor for moving components 2026-05-29 12:28:35 +02:00
Mikolaj Wielgus a6d1fe7025 Don't skip pins without net in Specctra import 2026-05-28 23:08:44 +02:00
Mikolaj Wielgus df5a9b57ec Make it possible to select whole components. Use different color for pin selection 2026-05-28 22:35:25 +02:00
34 changed files with 1227 additions and 527 deletions

View File

@ -3,8 +3,8 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{viewport::Viewport, workspace::GuiWorkspace};
use topola::Workspace;
use topola::primitives::{Joint, Polygon, Segment, Via};
use topola::{LayerId, Workspace};
pub struct Display {}
@ -54,6 +54,10 @@ impl Display {
board.pins_contain_joint(&workspace.workspace.selection().pins, joint_id);
let net_selected =
board.nets_contain_joint(&workspace.workspace.selection().nets, joint_id);
let component_selected = board.components_contain_joint(
&workspace.workspace.selection().components,
joint_id,
);
self.paint_joint(
ctx,
ui,
@ -62,7 +66,8 @@ impl Display {
workspace.appearance_panel.layer_color(
ctx,
board.layer_desc(joint.spec.layer),
pin_selected || (joint.spec.pin.is_none() && net_selected),
pin_selected,
(joint.spec.pin.is_none() && net_selected) || component_selected,
),
);
}
@ -73,6 +78,10 @@ impl Display {
board.pins_contain_segment(&workspace.workspace.selection().pins, segment_id);
let net_selected =
board.nets_contain_segment(&workspace.workspace.selection().nets, segment_id);
let component_selected = board.components_contain_segment(
&workspace.workspace.selection().components,
segment_id,
);
self.paint_segment(
ctx,
ui,
@ -81,7 +90,8 @@ impl Display {
workspace.appearance_panel.layer_color(
ctx,
board.layer_desc(segment.layer),
pin_selected || (segment.spec.pin.is_none() && net_selected),
pin_selected,
(segment.spec.pin.is_none() && net_selected) || component_selected,
),
);
}
@ -92,6 +102,8 @@ impl Display {
board.pins_contain_via(&workspace.workspace.selection().pins, via_id);
let net_selected =
board.nets_contain_via(&workspace.workspace.selection().nets, via_id);
let component_selected = board
.components_contain_via(&workspace.workspace.selection().components, via_id);
self.paint_via(
ctx,
ui,
@ -100,7 +112,8 @@ impl Display {
workspace.appearance_panel.layer_color(
ctx,
board.layer_desc(layer),
pin_selected || (via.spec.pin.is_none() && net_selected),
pin_selected,
(via.spec.pin.is_none() && net_selected) || component_selected,
),
);
}
@ -111,6 +124,10 @@ impl Display {
board.pins_contain_polygon(&workspace.workspace.selection().pins, polygon_id);
let net_selected =
board.nets_contain_polygon(&workspace.workspace.selection().nets, polygon_id);
let component_selected = board.components_contain_polygon(
&workspace.workspace.selection().components,
polygon_id,
);
self.paint_polygon(
ctx,
ui,
@ -119,7 +136,8 @@ impl Display {
workspace.appearance_panel.layer_color(
ctx,
board.layer_desc(polygon.layer),
pin_selected || (polygon.pin.is_none() && net_selected),
pin_selected,
(polygon.pin.is_none() && net_selected) || component_selected,
),
);
}
@ -225,17 +243,11 @@ impl Display {
}
for joint_id in layout.layer_joints(layer) {
let joint = layout.joint(joint_id);
let bbox = layout.joint(joint_id).bbox();
ui.painter().rect_stroke(
egui::Rect {
min: egui::pos2(
joint.bbox().lower()[0] as f32,
joint.bbox().lower()[1] as f32,
),
max: egui::pos2(
joint.bbox().upper()[0] as f32,
joint.bbox().upper()[1] as f32,
),
min: egui::pos2(bbox.min.x as f32, bbox.min.y as f32),
max: egui::pos2(bbox.max.x as f32, bbox.max.y as f32),
},
egui::CornerRadius::ZERO,
egui::Stroke::new(5.0, egui::Color32::GRAY),
@ -263,8 +275,8 @@ impl Display {
ui.painter().rect_stroke(
egui::Rect {
min: egui::pos2(bbox.lower()[0] as f32, bbox.lower()[1] as f32),
max: egui::pos2(bbox.upper()[0] as f32, bbox.upper()[1] as f32),
min: egui::pos2(bbox.min.x as f32, bbox.min.y as f32),
max: egui::pos2(bbox.max.x as f32, bbox.max.y as f32),
},
egui::CornerRadius::ZERO,
egui::Stroke::new(5.0, egui::Color32::GRAY),
@ -278,8 +290,8 @@ impl Display {
ui.painter().rect_stroke(
egui::Rect {
min: egui::pos2(bbox.lower()[0] as f32, bbox.lower()[1] as f32),
max: egui::pos2(bbox.upper()[0] as f32, bbox.upper()[1] as f32),
min: egui::pos2(bbox.min.x as f32, bbox.min.y as f32),
max: egui::pos2(bbox.max.x as f32, bbox.max.y as f32),
},
egui::CornerRadius::ZERO,
egui::Stroke::new(5.0, egui::Color32::GRAY),

View File

@ -31,6 +31,8 @@ impl ColorLayers {
pub struct LayerColors {
pub normal: egui::Color32,
pub highlighted: egui::Color32,
pub pin_selected: egui::Color32,
pub pin_selected_highlighted: egui::Color32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -58,26 +60,34 @@ impl LayersPanel {
continue;
};
let color = match layer_desc.typ {
let dark_light_colors = match layer_desc.typ {
LayerType::Copper => match layer_desc.side {
LayerSide::Top => Some((
LayerColors {
normal: egui::Color32::from_rgb(255, 52, 52),
highlighted: egui::Color32::from_rgb(255, 100, 100),
pin_selected: egui::Color32::from_rgb(190, 200, 70),
pin_selected_highlighted: egui::Color32::from_rgb(210, 240, 110),
},
LayerColors {
normal: egui::Color32::from_rgb(255, 27, 27),
highlighted: egui::Color32::from_rgb(255, 52, 52),
pin_selected: egui::Color32::from_rgb(160, 170, 50),
pin_selected_highlighted: egui::Color32::from_rgb(190, 210, 80),
},
)),
LayerSide::Bottom => Some((
LayerColors {
normal: egui::Color32::from_rgb(52, 52, 255),
highlighted: egui::Color32::from_rgb(100, 100, 255),
pin_selected: egui::Color32::from_rgb(70, 190, 190),
pin_selected_highlighted: egui::Color32::from_rgb(100, 230, 230),
},
LayerColors {
normal: egui::Color32::from_rgb(27, 27, 255),
highlighted: egui::Color32::from_rgb(52, 52, 255),
pin_selected: egui::Color32::from_rgb(50, 160, 160),
pin_selected_highlighted: egui::Color32::from_rgb(70, 200, 200),
},
)),
LayerSide::Inner => (layer_desc.index % 2 == 0)
@ -85,36 +95,65 @@ impl LayersPanel {
LayerColors {
normal: egui::Color32::from_rgb(127, 200, 127),
highlighted: egui::Color32::from_rgb(213, 236, 213),
pin_selected: egui::Color32::from_rgb(100, 230, 100),
pin_selected_highlighted: egui::Color32::from_rgb(170, 250, 170),
},
LayerColors {
normal: egui::Color32::from_rgb(76, 169, 76),
highlighted: egui::Color32::from_rgb(127, 200, 127),
pin_selected: egui::Color32::from_rgb(60, 200, 60),
pin_selected_highlighted: egui::Color32::from_rgb(100, 230, 100),
},
))
.or(Some((
LayerColors {
normal: egui::Color32::from_rgb(206, 125, 44),
highlighted: egui::Color32::from_rgb(232, 195, 158),
pin_selected: egui::Color32::from_rgb(170, 200, 70),
pin_selected_highlighted: egui::Color32::from_rgb(200, 230, 120),
},
LayerColors {
normal: egui::Color32::from_rgb(183, 80, 12),
highlighted: egui::Color32::from_rgb(206, 125, 44),
pin_selected: egui::Color32::from_rgb(140, 170, 50),
pin_selected_highlighted: egui::Color32::from_rgb(170, 200, 80),
},
))),
},
LayerType::Outline => Some((
LayerColors {
normal: egui::Color32::from_rgb(255, 255, 255),
highlighted: egui::Color32::from_rgb(255, 255, 255),
},
LayerColors {
normal: egui::Color32::from_rgb(255, 255, 255),
highlighted: egui::Color32::from_rgb(255, 255, 255),
},
)),
LayerType::Outline => match layer_desc.side {
LayerSide::Top => Some((
LayerColors {
normal: egui::Color32::from_rgb(222, 217, 141),
highlighted: egui::Color32::from_rgb(255, 255, 215),
pin_selected: egui::Color32::from_rgb(170, 235, 100),
pin_selected_highlighted: egui::Color32::from_rgb(210, 255, 160),
},
LayerColors {
normal: egui::Color32::from_rgb(185, 180, 110),
highlighted: egui::Color32::from_rgb(245, 240, 165),
pin_selected: egui::Color32::from_rgb(140, 210, 80),
pin_selected_highlighted: egui::Color32::from_rgb(190, 245, 130),
},
)),
LayerSide::Bottom => Some((
LayerColors {
normal: egui::Color32::from_rgb(212, 158, 147),
highlighted: egui::Color32::from_rgb(255, 228, 210),
pin_selected: egui::Color32::from_rgb(160, 210, 110),
pin_selected_highlighted: egui::Color32::from_rgb(200, 245, 165),
},
LayerColors {
normal: egui::Color32::from_rgb(170, 130, 120),
highlighted: egui::Color32::from_rgb(235, 195, 185),
pin_selected: egui::Color32::from_rgb(130, 190, 85),
pin_selected_highlighted: egui::Color32::from_rgb(175, 230, 140),
},
)),
LayerSide::Inner => None,
},
};
if let Some((dark_color, light_color)) = color {
if let Some((dark_color, light_color)) = dark_light_colors {
dark_layer_colors.insert(layer_desc.clone(), dark_color);
light_layer_colors.insert(layer_desc.clone(), light_color);
}
@ -125,6 +164,8 @@ impl LayersPanel {
default: LayerColors {
normal: egui::Color32::from_rgb(255, 255, 255),
highlighted: egui::Color32::from_rgb(255, 255, 255),
pin_selected: egui::Color32::from_rgb(200, 255, 200),
pin_selected_highlighted: egui::Color32::from_rgb(220, 255, 220),
},
colors: dark_layer_colors,
},
@ -134,6 +175,8 @@ impl LayersPanel {
default: LayerColors {
normal: egui::Color32::from_rgb(0, 0, 0),
highlighted: egui::Color32::from_rgb(0, 0, 0),
pin_selected: egui::Color32::from_rgb(0, 100, 0),
pin_selected_highlighted: egui::Color32::from_rgb(0, 140, 0),
},
colors: light_layer_colors,
},
@ -199,12 +242,20 @@ impl LayersPanel {
&self,
ctx: &Context,
layer_desc: Option<&LayerDesc>,
pin_selected: bool,
highlight: bool,
) -> egui::Color32 {
if highlight {
self.colors(ctx).layers.colors(layer_desc).highlighted
let colors = self.colors(ctx).layers.colors(layer_desc);
if pin_selected {
if highlight {
colors.pin_selected_highlighted
} else {
colors.pin_selected
}
} else if highlight {
colors.highlighted
} else {
self.colors(ctx).layers.colors(layer_desc).normal
colors.normal
}
}

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use egui::Pos2;
use topola::{InteractiveInput, MasterInteractor, Vector2, Workspace};
use topola::{MasterInteractor, Vector2, Workspace};
use crate::{display::Display, workspace::GuiWorkspace};
@ -51,7 +51,16 @@ impl Viewport {
Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into());
if let Some(workspace) = workspace {
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
let escape_pressed = ctx.input(|i| i.key_pressed(egui::Key::Escape));
if escape_pressed {
if let Some(interactor) = self.master_interactor.as_mut() {
let board = match &mut workspace.workspace {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_) => panic!("expected board workspace"),
};
interactor.abort(board);
*workspace.workspace.selection_mut() = interactor.selection().clone();
}
self.master_interactor = None;
}
@ -75,7 +84,6 @@ impl Viewport {
if primary_pressed && response.hovered() {
self.master_interactor = Some(MasterInteractor::new(
None,
workspace.workspace.selection().clone(),
));
}
@ -86,16 +94,16 @@ impl Viewport {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_) => panic!("expected board workspace"),
};
interactor.update(
interactor.hold(
board,
workspace.appearance_panel.active,
InteractiveInput::new(pointer_on_scene, false, false, false),
pointer_on_scene,
);
if let Some(selection_interactor) =
interactor.selection_interactor().as_ref()
if let Some(select_interactor) =
interactor.select_interactor().as_ref()
{
let origin = *selection_interactor.origin();
let origin = *select_interactor.origin();
let drag_rect_scene = egui::Rect::from_min_max(
egui::pos2(
origin.x.min(pointer_on_scene.x) as f32,
@ -133,19 +141,19 @@ impl Viewport {
if let Some(mut interactor) = self.master_interactor.take() {
let pointer_for_scene = maybe_pointer_on_scene.unwrap_or_else(|| {
interactor
.selection_interactor()
.select_interactor()
.as_ref()
.map(|selection_interactor| *selection_interactor.origin())
.map(|select_interactor| *select_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(
interactor.release(
board,
workspace.appearance_panel.active,
InteractiveInput::new(pointer_for_scene, true, false, false),
pointer_for_scene,
);
*workspace.workspace.selection_mut() = interactor.selection().clone();
@ -153,19 +161,13 @@ impl Viewport {
}
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());
MasterInteractor::new(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),
);
interactor.delete(board);
*workspace.workspace.selection_mut() = interactor.selection().clone();
}

15
topola/src/board/bbox.rs Normal file
View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{Board, Rect2, layout::compounds::ComponentId, selections::ComponentSelection};
impl Board {
pub fn components_bbox2(&self, selection: ComponentSelection) -> Option<Rect2<i64>> {
self.resolved_components_bbox2(&self.resolve_components(selection).collect::<Vec<_>>())
}
pub fn resolved_components_bbox2(&self, selection: &[ComponentId]) -> Option<Rect2<i64>> {
self.layout.components_bbox2(selection.iter().copied())
}
}

View File

@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{
Board, Vector2,
primitives::{JointId, PolygonId, SegmentId, ViaId},
selections::{ComponentSelection, NetSelection, NetSelector, PinSelection},
};
impl Board {
pub fn components_contain_point(
&self,
selection: &ComponentSelection,
point: Vector2<i64>,
) -> bool {
let Some(bbox) = self.components_bbox2(selection.clone()) else {
return false;
};
bbox.contains_point(point)
}
pub fn components_contain_joint(&self, selection: &ComponentSelection, id: JointId) -> bool {
let Some(selector) = self.joint_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_segment(
&self,
selection: &ComponentSelection,
id: SegmentId,
) -> bool {
let Some(selector) = self.segment_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_via(&self, selection: &ComponentSelection, id: ViaId) -> bool {
let Some(selector) = self.via_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_polygon(
&self,
selection: &ComponentSelection,
id: PolygonId,
) -> bool {
let Some(selector) = self.polygon_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn nets_contain_joint(&self, selection: &NetSelection, id: JointId) -> bool {
let joint = self.layout.joint(id);
let Some(net_name) = joint.spec.net.and_then(|net| self.net_name(net)) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_segment(&self, selection: &NetSelection, id: SegmentId) -> bool {
let segment = self.layout.segment(id);
let Some(net_name) = segment.net.and_then(|net| self.net_name(net)) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_via(&self, selection: &NetSelection, id: ViaId) -> bool {
let via = self.layout.via(id);
let Some(net_name) = via.net.and_then(|net| self.net_name(net)) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_polygon(&self, selection: &NetSelection, id: PolygonId) -> bool {
let polygon = self.layout.polygon(id);
let Some(net_name) = polygon.net.and_then(|net| self.net_name(net)) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn pins_contain_joint(&self, selection: &PinSelection, id: JointId) -> bool {
let Some(selector) = self.joint_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_segment(&self, selection: &PinSelection, id: SegmentId) -> bool {
let Some(selector) = self.segment_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_via(&self, selection: &PinSelection, id: ViaId) -> bool {
let Some(selector) = self.via_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_polygon(&self, selection: &PinSelection, id: PolygonId) -> bool {
let Some(selector) = self.polygon_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
}

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::Constructor;
use undoredo::DiscardDelta;
use crate::{Board, LayerId, Vector2, selections::ComponentSelection};
#[derive(Clone, Constructor, Debug, Eq, Getters, PartialEq)]
pub struct DragMoveInteractor {
origin: Vector2<i64>,
layer: LayerId,
selection: ComponentSelection,
}
impl DragMoveInteractor {
pub fn abort(&mut self, board: &mut Board) {
board.discard_delta();
}
pub fn hold(&mut self, board: &mut Board, pointer: Vector2<i64>) {
board.discard_delta();
board.move_components_by(self.selection.clone(), pointer - self.origin);
}
pub fn release(&mut self, board: &mut Board, pointer: Vector2<i64>) {
self.hold(board, pointer);
}
}

View File

@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};
use crate::{
Rect3, Vector2, Vector3,
board::{
Board,
interactors::{SelectionCombineMode, SelectionContainMode},
selections::PersistableSelection,
},
layout::LayerId,
};
#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct DragSelectOptions {
combine: SelectionCombineMode,
contain: SelectionContainMode,
}
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct DragSelectInteractor {
origin: Vector2<i64>,
layer: LayerId,
original_selection: PersistableSelection,
selection: PersistableSelection,
options: DragSelectOptions,
}
impl DragSelectInteractor {
pub fn new(
origin: Vector2<i64>,
layer: LayerId,
original_selection: PersistableSelection,
options: DragSelectOptions,
) -> Self {
Self {
origin,
layer,
original_selection,
selection: PersistableSelection::new(),
options,
}
}
pub fn abort(&mut self) {
self.selection = self.original_selection.clone();
}
pub fn hold(&mut self, board: &Board, pointer: Vector2<i64>) {
self.selection = PersistableSelection::new();
let rect = Rect3::new(
Vector3::new(self.origin.x, self.origin.y, self.layer.index() as i64),
Vector3::new(pointer.x, pointer.y, self.layer.index() as i64),
);
let all_belong_to_pins = match self.options.contain {
SelectionContainMode::Crossing => {
board
.layout()
.locate_joints_prefer_layer_intersecting_rect(rect)
.all(|joint_id| board.layout().joint(joint_id).spec.pin.is_some())
&& board
.layout()
.locate_segments_prefer_layer_intersecting_rect(rect)
.all(|segment_id| board.layout().segment(segment_id).spec.pin.is_some())
&& board
.layout()
.locate_vias_prefer_layer_intersecting_rect(rect)
.all(|via_id| board.layout().via(via_id).spec.pin.is_some())
&& board
.layout()
.locate_polygons_prefer_layer_intersecting_rect(rect)
.all(|polygon_id| board.layout().polygon(polygon_id).pin.is_some())
}
SelectionContainMode::Window => {
board
.layout()
.locate_joints_prefer_layer_inside_rect(rect)
.all(|joint_id| board.layout().joint(joint_id).spec.pin.is_some())
&& board
.layout()
.locate_segments_prefer_layer_inside_rect(rect)
.all(|segment_id| board.layout().segment(segment_id).spec.pin.is_some())
&& board
.layout()
.locate_vias_prefer_layer_inside_rect(rect)
.all(|via_id| board.layout().via(via_id).spec.pin.is_some())
&& board
.layout()
.locate_polygons_prefer_layer_inside_rect(rect)
.all(|polygon_id| board.layout().polygon(polygon_id).pin.is_some())
}
};
if !all_belong_to_pins {
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.components
.add(board.locate_components_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.components
.add(board.locate_components_prefer_layer_inside_rect(rect));
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.nets
.add(board.locate_nets_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.nets
.add(board.locate_nets_prefer_layer_inside_rect(rect));
}
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.pins
.add(board.locate_pins_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.pins
.add(board.locate_pins_prefer_layer_inside_rect(rect));
}
}
// TODO: There's no need to clone on each update.
let mut combined_selection = self.original_selection.clone();
match self.options.combine {
SelectionCombineMode::Replace => {
combined_selection.components = self.selection.components.clone();
combined_selection.nets = self.selection.nets.clone();
combined_selection.pins = self.selection.pins.clone();
}
SelectionCombineMode::Additive => {
combined_selection
.pins
.add(self.selection.pins.0.iter().cloned());
combined_selection
.nets
.add(self.selection.nets.0.iter().cloned());
combined_selection
.components
.add(self.selection.components.0.iter().cloned());
}
SelectionCombineMode::Subtractive => {
combined_selection
.pins
.sub(self.selection.pins.0.iter().cloned());
combined_selection
.nets
.sub(self.selection.nets.0.iter().cloned());
combined_selection
.components
.sub(self.selection.components.0.iter().cloned());
}
SelectionCombineMode::Toggle => {
combined_selection
.components
.toggle(self.selection.components.0.iter().cloned());
combined_selection
.nets
.toggle(self.selection.nets.0.iter().cloned());
combined_selection
.pins
.toggle(self.selection.pins.0.iter().cloned());
}
}
self.selection = combined_selection;
}
}

View File

@ -1,147 +0,0 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};
use crate::{
Rect3, Vector2, Vector3,
board::{
Board,
interactors::{InteractiveInput, SelectionCombineMode, SelectionContainMode},
selections::PersistableSelection,
},
layout::LayerId,
};
#[derive(Clone, Constructor, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DragSelectionOptions {
combine: SelectionCombineMode,
contain: SelectionContainMode,
}
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct DragSelectionInteractor {
origin: Vector2<i64>,
layer: LayerId,
original_selection: PersistableSelection,
selection: PersistableSelection,
options: DragSelectionOptions,
}
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) {
if input.cancel {
self.selection = self.original_selection.clone();
return;
}
self.selection = PersistableSelection::new();
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_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.components
.add(board.locate_components_prefer_layer_inside_rect(rect));
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.nets
.add(board.locate_nets_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.nets
.add(board.locate_nets_prefer_layer_inside_rect(rect));
}
}
match self.options.contain {
SelectionContainMode::Crossing => {
self.selection
.pins
.add(board.locate_pins_prefer_layer_intersecting_rect(rect));
}
SelectionContainMode::Window => {
self.selection
.pins
.add(board.locate_pins_prefer_layer_inside_rect(rect));
}
}
// TODO: There's no need to clone on each update.
let mut combined_selection = self.original_selection.clone();
match self.options.combine {
SelectionCombineMode::Replace => {
combined_selection.components = self.selection.components.clone();
combined_selection.nets = self.selection.nets.clone();
combined_selection.pins = self.selection.pins.clone();
}
SelectionCombineMode::Additive => {
combined_selection
.pins
.add(self.selection.pins.0.iter().cloned());
combined_selection
.nets
.add(self.selection.nets.0.iter().cloned());
combined_selection
.components
.add(self.selection.components.0.iter().cloned());
}
SelectionCombineMode::Subtractive => {
combined_selection
.pins
.sub(self.selection.pins.0.iter().cloned());
combined_selection
.nets
.sub(self.selection.nets.0.iter().cloned());
combined_selection
.components
.sub(self.selection.components.0.iter().cloned());
}
SelectionCombineMode::Toggle => {
combined_selection
.components
.xor(self.selection.components.0.iter().cloned());
combined_selection
.nets
.xor(self.selection.nets.0.iter().cloned());
combined_selection
.pins
.xor(self.selection.pins.0.iter().cloned());
}
}
self.selection = combined_selection;
}
}

View File

@ -3,45 +3,85 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_getters::Getters;
use derive_more::Constructor;
use crate::{
InteractiveInput,
Vector2,
board::{
Board,
interactors::{SelectionCombineMode, SelectionInteractor},
interactors::{DragMoveInteractor, SelectInteractor, SelectionCombineMode},
selections::PersistableSelection,
},
layout::LayerId,
};
#[derive(Clone, Constructor, Debug, Eq, Getters, PartialEq)]
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct MasterInteractor {
selection_interactor: Option<SelectionInteractor>,
select_interactor: Option<SelectInteractor>,
drag_move_interactor: Option<DragMoveInteractor>,
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;
pub fn new(selection: PersistableSelection) -> Self {
Self {
select_interactor: None,
drag_move_interactor: None,
selection,
}
}
pub fn delete(&mut self, board: &mut Board) {
board.delete_net_free_primitives(self.selection.nets.clone());
}
pub fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
if self.select_interactor.is_none() && self.drag_move_interactor.is_none() {
if board.components_contain_point(&self.selection.components, pointer) {
self.drag_move_interactor = Some(DragMoveInteractor::new(
pointer,
layer,
self.selection.components.clone(),
));
} else {
self.select_interactor = Some(SelectInteractor::new(
pointer,
self.selection.clone(),
SelectionCombineMode::Replace,
));
}
}
if let Some(drag_move_interactor) = self.drag_move_interactor.as_mut() {
drag_move_interactor.hold(board, pointer);
} else if let Some(select_interactor) = self.select_interactor.as_mut() {
select_interactor.hold(board, layer, pointer);
self.selection = select_interactor.selection().clone();
}
}
pub fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
if let Some(drag_move_interactor) = self.drag_move_interactor.as_mut() {
drag_move_interactor.release(board, pointer);
} else if let Some(select_interactor) = self.select_interactor.as_mut() {
select_interactor.release(board, layer, pointer);
self.selection = select_interactor.selection().clone();
}
self.select_interactor = None;
self.drag_move_interactor = None;
}
pub fn abort(&mut self, board: &mut Board) {
if let Some(drag_move_interactor) = self.drag_move_interactor.as_mut() {
drag_move_interactor.abort(board);
}
if let Some(select_interactor) = self.select_interactor.as_mut() {
select_interactor.abort();
self.selection = select_interactor.original_selection().clone();
}
self.select_interactor = None;
self.drag_move_interactor = None;
}
}

View File

@ -2,26 +2,17 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod drag_selection;
mod drag_move;
mod drag_select;
mod master;
mod selection;
mod select;
use derive_more::Constructor;
pub use drag_selection::{DragSelectionInteractor, DragSelectionOptions};
pub use drag_move::DragMoveInteractor;
pub use drag_select::{DragSelectInteractor, DragSelectOptions};
pub use master::MasterInteractor;
pub use selection::SelectionInteractor;
pub use select::SelectInteractor;
use serde::{Deserialize, Serialize};
use crate::Vector2;
#[derive(Clone, Constructor, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct InteractiveInput {
pointer: Vector2<i64>,
release: bool,
delete: bool,
cancel: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum SelectionCombineMode {
Replace,

View File

@ -9,8 +9,7 @@ use crate::{
board::{
Board,
interactors::{
DragSelectionInteractor, DragSelectionOptions, InteractiveInput, SelectionCombineMode,
SelectionContainMode,
DragSelectInteractor, DragSelectOptions, SelectionCombineMode, SelectionContainMode,
},
selections::PersistableSelection,
},
@ -18,14 +17,14 @@ use crate::{
};
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct SelectionInteractor {
pub struct SelectInteractor {
origin: Vector2<i64>,
original_selection: PersistableSelection,
selection: PersistableSelection,
combine: SelectionCombineMode,
}
impl SelectionInteractor {
impl SelectInteractor {
pub fn new(
origin: Vector2<i64>,
original_selection: PersistableSelection,
@ -39,48 +38,48 @@ impl SelectionInteractor {
}
}
pub fn update(&mut self, board: &Board, layer: LayerId, input: InteractiveInput) {
if input.cancel {
self.selection = self.original_selection.clone();
return;
}
pub fn abort(&mut self) {
self.selection = self.original_selection.clone();
}
if input.release && input.pointer == self.origin {
pub fn hold(&mut self, board: &Board, layer: LayerId, pointer: Vector2<i64>) {
let contain = if pointer.x >= self.origin.x {
SelectionContainMode::Window
} else {
SelectionContainMode::Crossing
};
let options = DragSelectOptions::new(self.combine.clone(), contain);
let mut drag_selection_interactor =
DragSelectInteractor::new(self.origin, layer, self.original_selection.clone(), options);
drag_selection_interactor.hold(board, pointer);
self.selection = drag_selection_interactor.selection().clone();
}
pub fn release(&mut self, board: &Board, layer: LayerId, pointer: Vector2<i64>) {
if pointer == self.origin {
let mut selection = self.original_selection.clone();
let point = Vector3::new(input.pointer.x, input.pointer.y, layer.index() as i64);
let point = Vector3::new(pointer.x, pointer.y, layer.index() as i64);
// Pins have intentional precedence over nets and components.
if let Some(pin_selector) = board.locate_pins_prefer_layer_at_point(point).next() {
selection.pins.xor(std::iter::once(pin_selector));
} else if let Some(net_selector) = board.locate_nets_prefer_layer_at_point(point).next() {
selection.nets.xor(std::iter::once(net_selector));
selection.pins.toggle(std::iter::once(pin_selector));
} else if let Some(net_selector) = board.locate_nets_prefer_layer_at_point(point).next()
{
selection.nets.toggle(std::iter::once(net_selector));
} else if let Some(component_selector) =
board.locate_components_prefer_layer_at_point(point).next()
{
selection
.components
.xor(std::iter::once(component_selector));
.toggle(std::iter::once(component_selector));
}
self.selection = selection;
return;
}
let contain = if input.pointer.x >= self.origin.x {
SelectionContainMode::Window
} else {
SelectionContainMode::Crossing
};
let options = DragSelectionOptions::new(self.combine.clone(), contain);
let mut drag_selection_interactor = DragSelectionInteractor::new(
self.origin,
layer,
self.original_selection.clone(),
options,
);
drag_selection_interactor.update(board, input);
self.selection = drag_selection_interactor.selection().clone();
self.hold(board, layer, pointer);
}
}

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod bbox;
mod contains;
mod insert;
pub mod interactors;
mod layer;

View File

@ -6,8 +6,7 @@ use crate::{
board::{
Board,
selections::{
ComponentSelection, ComponentSelector, NetSelection, NetSelector, PinSelection,
PinSelector,
ComponentSelection, ComponentSelector, NetSelector, PinSelection, PinSelector,
},
},
primitives::{JointId, PolygonId, SegmentId, ViaId},
@ -78,46 +77,6 @@ impl Board {
component_selection
}
pub fn components_contain_joint(&self, selection: &ComponentSelection, id: JointId) -> bool {
let Some(selector) = self.joint_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_segment(
&self,
selection: &ComponentSelection,
id: SegmentId,
) -> bool {
let Some(selector) = self.segment_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_via(&self, selection: &ComponentSelection, id: ViaId) -> bool {
let Some(selector) = self.via_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn components_contain_polygon(
&self,
selection: &ComponentSelection,
id: PolygonId,
) -> bool {
let Some(selector) = self.polygon_component_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn joint_component_selector(&self, id: JointId) -> Option<ComponentSelector> {
let joint = self.layout.joint(id);
@ -150,55 +109,11 @@ impl Board {
})
}
pub fn nets_contain_joint(&self, selection: &NetSelection, id: JointId) -> bool {
let joint = self.layout.joint(id);
let Some(net_name) = self.net_name(joint.spec.net) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_segment(&self, selection: &NetSelection, id: SegmentId) -> bool {
let segment = self.layout.segment(id);
let Some(net_name) = self.net_name(segment.net) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_via(&self, selection: &NetSelection, id: ViaId) -> bool {
let via = self.layout.via(id);
let Some(net_name) = self.net_name(via.net) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn nets_contain_polygon(&self, selection: &NetSelection, id: PolygonId) -> bool {
let polygon = self.layout.polygon(id);
let Some(net_name) = self.net_name(polygon.net) else {
return false;
};
selection
.0
.contains(&NetSelector::new(net_name.to_string()))
}
pub fn joint_net_selector(&self, id: JointId) -> Option<NetSelector> {
let joint = self.layout.joint(id);
Some(NetSelector {
net: self.net_name(joint.spec.net)?.to_string(),
net: self.net_name(joint.spec.net?)?.to_string(),
})
}
@ -206,7 +121,7 @@ impl Board {
let segment = self.layout.segment(id);
Some(NetSelector {
net: self.net_name(segment.net)?.to_string(),
net: self.net_name(segment.net?)?.to_string(),
})
}
@ -214,7 +129,7 @@ impl Board {
let via = self.layout.via(id);
Some(NetSelector {
net: self.net_name(via.net)?.to_string(),
net: self.net_name(via.net?)?.to_string(),
})
}
@ -222,42 +137,10 @@ impl Board {
let polygon = self.layout.polygon(id);
Some(NetSelector {
net: self.net_name(polygon.net)?.to_string(),
net: self.net_name(polygon.net?)?.to_string(),
})
}
pub fn pins_contain_joint(&self, selection: &PinSelection, id: JointId) -> bool {
let Some(selector) = self.joint_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_segment(&self, selection: &PinSelection, id: SegmentId) -> bool {
let Some(selector) = self.segment_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_via(&self, selection: &PinSelection, id: ViaId) -> bool {
let Some(selector) = self.via_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn pins_contain_polygon(&self, selection: &PinSelection, id: PolygonId) -> bool {
let Some(selector) = self.polygon_pin_selector(id) else {
return false;
};
selection.0.contains(&selector)
}
pub fn joint_pin_selector(&self, id: JointId) -> Option<PinSelector> {
let joint = self.layout.joint(id);

View File

@ -32,7 +32,7 @@ impl ComponentSelection {
}
}
pub fn xor(&mut self, selectors: impl IntoIterator<Item = ComponentSelector>) {
pub fn toggle(&mut self, selectors: impl IntoIterator<Item = ComponentSelector>) {
for selector in selectors {
if self.0.contains(&selector) {
self.0.remove(&selector);

View File

@ -38,7 +38,7 @@ impl NetSelection {
}
}
pub fn xor(&mut self, selectors: impl IntoIterator<Item = NetSelector>) {
pub fn toggle(&mut self, selectors: impl IntoIterator<Item = NetSelector>) {
for selector in selectors {
if self.0.contains(&selector) {
self.0.remove(&selector);

View File

@ -35,7 +35,7 @@ impl PinSelection {
}
}
pub fn xor(&mut self, selectors: impl IntoIterator<Item = PinSelector>) {
pub fn toggle(&mut self, selectors: impl IntoIterator<Item = PinSelector>) {
for selector in selectors {
if self.0.contains(&selector) {
self.0.remove(&selector);

265
topola/src/compass.rs Normal file
View File

@ -0,0 +1,265 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::ops::{Add, Neg};
use num_traits::{One, Signed, Zero};
use serde::{Deserialize, Serialize};
use crate::Vector2;
pub trait CompassDirection<T>: Copy + PartialEq + Into<Vector2<T>> {
fn nearest_from_vector(vector: Vector2<T>) -> Self;
fn turn_clockwise(self) -> Self;
fn turn_counterclockwise(self) -> Self;
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum CardinalDirection {
East,
North,
West,
South,
}
impl<T: Copy + Neg<Output = T> + One + Zero> From<CardinalDirection> for Vector2<T> {
fn from(cardinal_direction: CardinalDirection) -> Vector2<T> {
match cardinal_direction {
CardinalDirection::East => [T::one(), T::zero()].into(),
CardinalDirection::North => [T::zero(), -T::one()].into(),
CardinalDirection::West => [-T::one(), T::zero()].into(),
CardinalDirection::South => [T::zero(), T::one()].into(),
}
}
}
impl<T: Copy + PartialOrd + Signed + Zero> CompassDirection<T> for CardinalDirection {
fn nearest_from_vector(vector: Vector2<T>) -> Self {
if vector.x.abs() > vector.y.abs() {
if vector.x > T::zero() {
Self::East
} else {
Self::West
}
} else if vector.y > T::zero() {
Self::South
} else {
Self::North
}
}
fn turn_clockwise(self) -> Self {
match self {
Self::East => Self::South,
Self::North => Self::East,
Self::West => Self::North,
Self::South => Self::West,
}
}
fn turn_counterclockwise(self) -> Self {
match self {
Self::East => Self::North,
Self::North => Self::West,
Self::West => Self::South,
Self::South => Self::East,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum OrdinalDirection {
NorthWest,
SouthWest,
SouthEast,
NorthEast,
}
impl<T: Copy + Neg<Output = T> + One + Zero> From<OrdinalDirection> for Vector2<T> {
fn from(ordinal_direction: OrdinalDirection) -> Vector2<T> {
match ordinal_direction {
OrdinalDirection::NorthEast => [T::one(), -T::one()].into(),
OrdinalDirection::NorthWest => [-T::one(), -T::one()].into(),
OrdinalDirection::SouthWest => [-T::one(), T::one()].into(),
OrdinalDirection::SouthEast => [T::one(), T::one()].into(),
}
}
}
impl<T: Copy + PartialOrd + Signed + Zero> CompassDirection<T> for OrdinalDirection {
fn nearest_from_vector(vector: Vector2<T>) -> Self {
let zero = T::zero();
if vector.x > zero && vector.y > zero {
Self::SouthEast
} else if vector.x > zero && vector.y < zero {
Self::NorthEast
} else if vector.x < zero && vector.y < zero {
Self::NorthWest
} else {
Self::SouthWest
}
}
fn turn_clockwise(self) -> Self {
match self {
Self::NorthWest => Self::NorthEast,
Self::NorthEast => Self::SouthEast,
Self::SouthEast => Self::SouthWest,
Self::SouthWest => Self::NorthWest,
}
}
fn turn_counterclockwise(self) -> Self {
match self {
Self::NorthWest => Self::SouthWest,
Self::SouthWest => Self::SouthEast,
Self::SouthEast => Self::NorthEast,
Self::NorthEast => Self::NorthWest,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum PrincipalWind {
North,
NorthWest,
West,
SouthWest,
South,
SouthEast,
East,
NorthEast,
}
impl<T: Copy + Neg<Output = T> + One + Zero> From<PrincipalWind> for Vector2<T> {
fn from(principal_wind: PrincipalWind) -> Vector2<T> {
match principal_wind {
PrincipalWind::North => [T::zero(), -T::one()].into(),
PrincipalWind::NorthWest => [-T::one(), -T::one()].into(),
PrincipalWind::West => [-T::one(), T::zero()].into(),
PrincipalWind::SouthWest => [-T::one(), T::one()].into(),
PrincipalWind::South => [T::zero(), T::one()].into(),
PrincipalWind::SouthEast => [T::one(), T::one()].into(),
PrincipalWind::East => [T::one(), T::zero()].into(),
PrincipalWind::NorthEast => [T::one(), -T::one()].into(),
}
}
}
impl<T: Copy + Add<Output = T> + PartialOrd + Signed + Zero> CompassDirection<T> for PrincipalWind {
fn nearest_from_vector(vector: Vector2<T>) -> Self {
if vector.x.is_zero() && vector.y.is_zero() {
panic!("Zero vector has no direction");
}
if vector.x.is_zero() {
return if vector.y < T::zero() {
Self::North
} else {
Self::South
};
}
if vector.y.is_zero() {
return if vector.x > T::zero() {
Self::East
} else {
Self::West
};
}
if vector.x > T::zero() && vector.y < T::zero() {
if vector.x.abs() > vector.y.abs() {
if vector.y.abs() + vector.y.abs() <= vector.x.abs() {
Self::East
} else {
Self::NorthEast
}
} else if vector.y.abs() > vector.x.abs() {
if vector.x.abs() + vector.x.abs() <= vector.y.abs() {
Self::North
} else {
Self::NorthEast
}
} else {
Self::NorthEast
}
} else if vector.x > T::zero() && vector.y > T::zero() {
if vector.x.abs() > vector.y.abs() {
if vector.y.abs() + vector.y.abs() <= vector.x.abs() {
Self::East
} else {
Self::SouthEast
}
} else if vector.y.abs() > vector.x.abs() {
if vector.x.abs() + vector.x.abs() <= vector.y.abs() {
Self::South
} else {
Self::SouthEast
}
} else {
Self::SouthEast
}
} else if vector.x < T::zero() && vector.y > T::zero() {
if vector.x.abs() > vector.y.abs() {
if vector.y.abs() + vector.y.abs() <= vector.x.abs() {
Self::West
} else {
Self::SouthWest
}
} else if vector.y.abs() > vector.x.abs() {
if vector.x.abs() + vector.x.abs() <= vector.y.abs() {
Self::South
} else {
Self::SouthWest
}
} else {
Self::SouthWest
}
} else {
if vector.x.abs() > vector.y.abs() {
if vector.y.abs() + vector.y.abs() <= vector.x.abs() {
Self::West
} else {
Self::NorthWest
}
} else if vector.y.abs() > vector.x.abs() {
if vector.x.abs() + vector.x.abs() <= vector.y.abs() {
Self::North
} else {
Self::NorthWest
}
} else {
Self::NorthWest
}
}
}
fn turn_clockwise(self) -> Self {
match self {
Self::North => Self::NorthEast,
Self::NorthEast => Self::East,
Self::East => Self::SouthEast,
Self::SouthEast => Self::South,
Self::South => Self::SouthWest,
Self::SouthWest => Self::West,
Self::West => Self::NorthWest,
Self::NorthWest => Self::North,
}
}
fn turn_counterclockwise(self) -> Self {
match self {
Self::North => Self::NorthWest,
Self::NorthWest => Self::West,
Self::West => Self::SouthWest,
Self::SouthWest => Self::South,
Self::South => Self::SouthEast,
Self::SouthEast => Self::East,
Self::East => Self::NorthEast,
Self::NorthEast => Self::North,
}
}
}

55
topola/src/layout/bbox.rs Normal file
View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2026 Topola contributors
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{
Rect2,
layout::{Layout, compounds::ComponentId},
};
impl Layout {
pub fn component_bbox2(&self, component_id: ComponentId) -> Option<Rect2<i64>> {
let component = self.component(component_id);
let mut maybe_accum_bbox2 = None::<Rect2<i64>>;
for &joint_id in &component.joints {
unionize(&mut maybe_accum_bbox2, self.joint(joint_id).bbox().xy());
}
for &segment_id in &component.segments {
unionize(&mut maybe_accum_bbox2, self.segment(segment_id).bbox().xy());
}
for &via_id in &component.vias {
unionize(&mut maybe_accum_bbox2, self.via(via_id).bbox().xy());
}
for &polygon_id in &component.polygons {
unionize(&mut maybe_accum_bbox2, self.polygon(polygon_id).bbox().xy());
}
maybe_accum_bbox2
}
pub fn components_bbox2(
&self,
component_ids: impl IntoIterator<Item = ComponentId>,
) -> Option<Rect2<i64>> {
let mut maybe_accum_bbox2 = None::<Rect2<i64>>;
for component_id in component_ids {
if let Some(component_bbox2) = self.component_bbox2(component_id) {
unionize(&mut maybe_accum_bbox2, component_bbox2);
}
}
maybe_accum_bbox2
}
}
fn unionize(maybe_accum_bbox2: &mut Option<Rect2<i64>>, bbox2_to_add: Rect2<i64>) {
*maybe_accum_bbox2 = Some(match maybe_accum_bbox2 {
None => bbox2_to_add,
Some(accum) => accum.union(bbox2_to_add),
})
}

View File

@ -30,7 +30,7 @@ impl ComponentId {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct Component {
pub joints: Vec<JointId>,
pub segments: Vec<SegmentId>,
@ -40,11 +40,6 @@ pub struct Component {
impl Component {
pub fn new() -> Self {
Self {
joints: Vec::new(),
segments: Vec::new(),
vias: Vec::new(),
polygons: Vec::new(),
}
Default::default()
}
}

View File

@ -32,7 +32,8 @@ impl Layout {
});
}
self.joints_rtree.remove(&GeomWithData::new(bbox, joint_id));
self.joints_rtree
.remove(&GeomWithData::new(bbox.rtree_rectangle(), joint_id));
self.joints.remove(&joint_id.index());
}
@ -63,7 +64,7 @@ impl Layout {
}
self.segments_rtree
.remove(&GeomWithData::new(bbox, segment_id));
.remove(&GeomWithData::new(bbox.rtree_rectangle(), segment_id));
self.segments.remove(&segment_id.index());
}
@ -89,7 +90,8 @@ impl Layout {
});
}
self.vias_rtree.remove(&GeomWithData::new(bbox, via_id));
self.vias_rtree
.remove(&GeomWithData::new(bbox.rtree_rectangle(), via_id));
self.vias.remove(&via_id.index());
}
@ -120,7 +122,7 @@ impl Layout {
}
self.polygons_rtree
.remove(&GeomWithData::new(bbox, polygon_id));
.remove(&GeomWithData::new(bbox.rtree_rectangle(), polygon_id));
self.polygons.remove(&polygon_id.index());
}
}

View File

@ -37,7 +37,7 @@ impl Layout {
let joint_id = JointId::new(self.joints.push(joint));
self.joints_rtree
.insert(GeomWithData::new(bbox, joint_id), ());
.insert(GeomWithData::new(bbox.rtree_rectangle(), joint_id), ());
if let Some(component_id) = component_id {
self.components.modify(component_id.index(), |component| {
@ -81,7 +81,7 @@ impl Layout {
});
self.segments_rtree
.insert(GeomWithData::new(bbox, segment_id), ());
.insert(GeomWithData::new(bbox.rtree_rectangle(), segment_id), ());
if let Some(component_id) = component_id {
self.components.modify(component_id.index(), |component| {
@ -122,7 +122,8 @@ impl Layout {
joint.vias.push(via_id)
});
self.vias_rtree.insert(GeomWithData::new(bbox, via_id), ());
self.vias_rtree
.insert(GeomWithData::new(bbox.rtree_rectangle(), via_id), ());
if let Some(component_id) = component_id {
self.components.modify(component_id.index(), |component| {
@ -145,7 +146,7 @@ impl Layout {
let polygon_id = PolygonId::new(self.polygons.push(polygon));
self.polygons_rtree
.insert(GeomWithData::new(bbox, polygon_id), ());
.insert(GeomWithData::new(bbox.rtree_rectangle(), polygon_id), ());
if let Some(component_id) = component_id {
self.components.modify(component_id.index(), |component| {

View File

@ -528,19 +528,27 @@ impl Layout {
let mut nets = BTreeSet::new();
for joint_id in self.locate_joints_prefer_layer_intersecting_rect(rect) {
nets.insert(self.joint(joint_id).spec.net);
if let Some(net) = self.joint(joint_id).spec.net {
nets.insert(net);
}
}
for segment_id in self.locate_segments_prefer_layer_intersecting_rect(rect) {
nets.insert(self.segment(segment_id).net);
if let Some(net) = self.segment(segment_id).net {
nets.insert(net);
}
}
for via_id in self.locate_vias_prefer_layer_intersecting_rect(rect) {
nets.insert(self.via(via_id).net);
if let Some(net) = self.via(via_id).net {
nets.insert(net);
}
}
for polygon_id in self.locate_polygons_prefer_layer_intersecting_rect(rect) {
nets.insert(self.polygon(polygon_id).net);
if let Some(net) = self.polygon(polygon_id).net {
nets.insert(net);
}
}
nets.into_iter()
@ -550,19 +558,27 @@ impl Layout {
let mut nets = BTreeSet::new();
for joint_id in self.locate_joints_intersecting_rect(rect) {
nets.insert(self.joint(joint_id).spec.net);
if let Some(net) = self.joint(joint_id).spec.net {
nets.insert(net);
}
}
for segment_id in self.locate_segments_intersecting_rect(rect) {
nets.insert(self.segment(segment_id).net);
if let Some(net) = self.segment(segment_id).net {
nets.insert(net);
}
}
for via_id in self.locate_vias_intersecting_rect(rect) {
nets.insert(self.via(via_id).net);
if let Some(net) = self.via(via_id).net {
nets.insert(net);
}
}
for polygon_id in self.locate_polygons_intersecting_rect(rect) {
nets.insert(self.polygon(polygon_id).net);
if let Some(net) = self.polygon(polygon_id).net {
nets.insert(net);
}
}
nets.into_iter()
@ -575,19 +591,27 @@ impl Layout {
let mut nets = BTreeSet::new();
for joint_id in self.locate_joints_prefer_layer_inside_rect(rect) {
nets.insert(self.joint(joint_id).spec.net);
if let Some(net) = self.joint(joint_id).spec.net {
nets.insert(net);
}
}
for segment_id in self.locate_segments_prefer_layer_inside_rect(rect) {
nets.insert(self.segment(segment_id).net);
if let Some(net) = self.segment(segment_id).net {
nets.insert(net);
}
}
for via_id in self.locate_vias_prefer_layer_inside_rect(rect) {
nets.insert(self.via(via_id).net);
if let Some(net) = self.via(via_id).net {
nets.insert(net);
}
}
for polygon_id in self.locate_polygons_prefer_layer_inside_rect(rect) {
nets.insert(self.polygon(polygon_id).net);
if let Some(net) = self.polygon(polygon_id).net {
nets.insert(net);
}
}
nets.into_iter()
@ -597,19 +621,27 @@ impl Layout {
let mut nets = BTreeSet::new();
for joint_id in self.locate_joints_inside_rect(rect) {
nets.insert(self.joint(joint_id).spec.net);
if let Some(net) = self.joint(joint_id).spec.net {
nets.insert(net);
}
}
for segment_id in self.locate_segments_inside_rect(rect) {
nets.insert(self.segment(segment_id).net);
if let Some(net) = self.segment(segment_id).net {
nets.insert(net);
}
}
for via_id in self.locate_vias_inside_rect(rect) {
nets.insert(self.via(via_id).net);
if let Some(net) = self.via(via_id).net {
nets.insert(net);
}
}
for polygon_id in self.locate_polygons_inside_rect(rect) {
nets.insert(self.polygon(polygon_id).net);
if let Some(net) = self.polygon(polygon_id).net {
nets.insert(net);
}
}
nets.into_iter()

View File

@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
mod bbox;
pub mod compounds;
mod delete;
mod insert;
@ -21,9 +22,9 @@ use stable_vec::StableVec;
use undoredo::aliases::RTreeHalfDelta;
use undoredo::{Delta, Recorder};
use crate::{
layout::compounds::{Component, Pin, PinId},
layout::primitives::{Joint, JointId, Polygon, PolygonId, Segment, SegmentId, Via, ViaId},
use crate::layout::{
compounds::{Component, ComponentId, Pin, PinId},
primitives::{Joint, JointId, Polygon, PolygonId, Segment, SegmentId, Via, ViaId},
};
#[derive(
@ -152,6 +153,14 @@ impl Layout {
)
}
pub fn component(&self, component_id: ComponentId) -> &Component {
&self.components[component_id.index()]
}
pub fn pin(&self, pin_id: PinId) -> &Pin {
&self.pins[pin_id.index()]
}
pub fn joint(&self, joint_id: JointId) -> &Joint {
&self.joints[joint_id.index()]
}
@ -167,8 +176,4 @@ impl Layout {
pub fn polygon(&self, polygon_id: PolygonId) -> &Polygon {
&self.polygons[polygon_id.index()]
}
pub fn pin(&self, pin_id: PinId) -> &Pin {
&self.pins[pin_id.index()]
}
}

View File

@ -34,13 +34,15 @@ impl Layout {
{
let old_joint = &self.joints[id.index()];
self.joints_rtree
.remove(&GeomWithData::new(old_joint.bbox(), id));
.remove(&GeomWithData::new(old_joint.bbox().rtree_rectangle(), id));
self.joints.modify(id.index(), |joint| f(joint));
let new_joint = self.joints[id.index()].clone();
self.joints_rtree
.insert(GeomWithData::new(new_joint.bbox(), id), ());
self.joints_rtree.insert(
GeomWithData::new(new_joint.bbox().rtree_rectangle(), id),
(),
);
}
pub fn modify_segment<F>(&mut self, id: SegmentId, f: F)
@ -49,20 +51,22 @@ impl Layout {
{
let old_segment = &self.segments[id.index()];
self.segments_rtree
.remove(&GeomWithData::new(old_segment.bbox(), id));
.remove(&GeomWithData::new(old_segment.bbox().rtree_rectangle(), id));
self.segments
.modify(id.index(), |segment| f(&mut segment.spec));
let new_segment = &self.segments[id.index()];
self.segments_rtree
.insert(GeomWithData::new(new_segment.bbox(), id), ());
self.segments_rtree.insert(
GeomWithData::new(new_segment.bbox().rtree_rectangle(), id),
(),
);
}
pub(super) fn update_segment(&mut self, id: SegmentId) {
let old_segment = &self.segments[id.index()];
self.segments_rtree
.remove(&GeomWithData::new(old_segment.bbox(), id));
.remove(&GeomWithData::new(old_segment.bbox().rtree_rectangle(), id));
let endjoint_ids = old_segment.spec.endjoints;
let endjoint_specs = [
@ -76,8 +80,10 @@ impl Layout {
});
let new_segment = &self.segments[id.index()];
self.segments_rtree
.insert(GeomWithData::new(new_segment.bbox(), id), ());
self.segments_rtree.insert(
GeomWithData::new(new_segment.bbox().rtree_rectangle(), id),
(),
);
}
pub fn modify_via<F>(&mut self, id: ViaId, f: F)
@ -86,19 +92,19 @@ impl Layout {
{
let old_via = &self.vias[id.index()];
self.vias_rtree
.remove(&GeomWithData::new(old_via.bbox(), id));
.remove(&GeomWithData::new(old_via.bbox().rtree_rectangle(), id));
self.vias.modify(id.index(), |via| f(&mut via.spec));
let new_via = &self.vias[id.index()];
self.vias_rtree
.insert(GeomWithData::new(new_via.bbox(), id), ());
.insert(GeomWithData::new(new_via.bbox().rtree_rectangle(), id), ());
}
pub(super) fn update_via(&mut self, id: ViaId) {
let old_via = &self.vias[id.index()];
self.vias_rtree
.remove(&GeomWithData::new(old_via.bbox(), id));
.remove(&GeomWithData::new(old_via.bbox().rtree_rectangle(), id));
let endjoint_ids = old_via.spec.endjoints;
let endjoint_specs = [
@ -114,7 +120,7 @@ impl Layout {
let new_via = &self.vias[id.index()];
self.vias_rtree
.insert(GeomWithData::new(new_via.bbox(), id), ());
.insert(GeomWithData::new(new_via.bbox().rtree_rectangle(), id), ());
}
pub fn modify_polygon<F>(&mut self, id: PolygonId, f: F)
@ -123,12 +129,14 @@ impl Layout {
{
let old_polygon = &self.polygons[id.index()];
self.polygons_rtree
.remove(&GeomWithData::new(old_polygon.bbox(), id));
.remove(&GeomWithData::new(old_polygon.bbox().rtree_rectangle(), id));
self.polygons.modify(id.index(), |polygon| f(polygon));
let new_polygon = &self.polygons[id.index()];
self.polygons_rtree
.insert(GeomWithData::new(new_polygon.bbox(), id), ());
self.polygons_rtree.insert(
GeomWithData::new(new_polygon.bbox().rtree_rectangle(), id),
(),
);
}
}

View File

@ -3,13 +3,12 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_more::{Constructor, From};
use rstar::{AABB, primitives::Rectangle};
use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::primitives::{SegmentId, ViaId};
use crate::vector::Vector2;
use crate::{Rect3, Vector3, layout::LayerId};
#[derive(
Clone,
@ -40,7 +39,7 @@ pub struct JointSpec {
pub position: Vector2<i64>,
pub layer: LayerId,
pub radius: u64,
pub net: NetId,
pub net: Option<NetId>,
pub component: Option<ComponentId>,
pub pin: Option<PinId>,
}
@ -57,19 +56,22 @@ impl Joint {
self.spec.position
}
pub fn bbox(&self) -> Rectangle<[i64; 3]> {
Rectangle::from_aabb(AABB::from_corners(
[
self.spec.position.x - self.spec.radius as i64,
self.spec.position.y - self.spec.radius as i64,
self.spec.layer.index() as i64,
],
[
self.spec.position.x + self.spec.radius as i64,
self.spec.position.y + self.spec.radius as i64,
self.spec.layer.index() as i64,
],
))
pub fn bbox(&self) -> Rect3<i64> {
let radius = self.spec.radius as i64;
let layer = self.spec.layer.index() as i64;
Rect3::new(
Vector3::new(
self.spec.position.x - radius,
self.spec.position.y - radius,
layer,
),
Vector3::new(
self.spec.position.x + radius,
self.spec.position.y + radius,
layer,
),
)
}
pub fn contains_point2(&self, point: Vector2<i64>) -> bool {

View File

@ -3,12 +3,11 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_more::{Constructor, From};
use rstar::{AABB, Envelope, primitives::Rectangle};
use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::vector::Vector2;
use crate::{Rect3, Vector3, layout::LayerId};
#[derive(
Clone,
@ -38,23 +37,32 @@ impl PolygonId {
pub struct Polygon {
pub vertices: Vec<Vector2<i64>>,
pub layer: LayerId,
pub net: NetId,
pub net: Option<NetId>,
pub component: Option<ComponentId>,
pub pin: Option<PinId>,
}
impl Polygon {
pub fn bbox(&self) -> Rectangle<[i64; 3]> {
Rectangle::from_aabb(self.vertices.clone().into_iter().fold(
AABB::new_empty(),
|aabb, vertex| {
aabb.merged(&AABB::from_point([
vertex.x,
vertex.y,
self.layer.index() as i64,
]))
},
))
pub fn bbox(&self) -> Rect3<i64> {
let layer = self.layer.index() as i64;
let mut min = Vector2::new(i64::MAX, i64::MAX);
let mut max = Vector2::new(i64::MIN, i64::MIN);
for vertex in &self.vertices {
min.x = std::cmp::min(min.x, vertex.x);
min.y = std::cmp::min(min.y, vertex.y);
max.x = std::cmp::max(max.x, vertex.x);
max.y = std::cmp::max(max.y, vertex.y);
}
if self.vertices.is_empty() {
return Rect3::new(Vector3::new(0, 0, layer), Vector3::new(0, 0, layer));
}
Rect3::new(
Vector3::new(min.x, min.y, layer),
Vector3::new(max.x, max.y, layer),
)
}
pub fn center(&self) -> Vector2<i64> {

View File

@ -3,12 +3,11 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_more::{Constructor, From};
use rstar::primitives::Rectangle;
use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::vector::Vector2;
use crate::{Rect3, Vector3, layout::LayerId};
use super::JointId;
@ -49,7 +48,7 @@ pub struct Segment {
pub spec: SegmentSpec,
pub endpoints: [Vector2<i64>; 2],
pub layer: LayerId,
pub net: NetId,
pub net: Option<NetId>,
}
impl Segment {
@ -80,16 +79,22 @@ impl Segment {
)
}
pub fn bbox(&self) -> Rectangle<[i64; 3]> {
pub fn bbox(&self) -> Rect3<i64> {
let endpoints = self.endpoints;
let layer = self.layer.index() as i64;
let half_width = self.spec.half_width as i64;
let min_x = std::cmp::min(endpoints[0].x, endpoints[1].x) - half_width;
let min_y = std::cmp::min(endpoints[0].y, endpoints[1].y) - half_width;
let max_x = std::cmp::max(endpoints[0].x, endpoints[1].x) + half_width;
let max_y = std::cmp::max(endpoints[0].y, endpoints[1].y) + half_width;
Rectangle::from_corners([min_x, min_y, layer], [max_x, max_y, layer])
Rect3::new(
Vector3::new(
std::cmp::min(endpoints[0].x, endpoints[1].x) - half_width,
std::cmp::min(endpoints[0].y, endpoints[1].y) - half_width,
layer,
),
Vector3::new(
std::cmp::max(endpoints[0].x, endpoints[1].x) + half_width,
std::cmp::max(endpoints[0].y, endpoints[1].y) + half_width,
layer,
),
)
}
}

View File

@ -3,12 +3,11 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use derive_more::{Constructor, From};
use rstar::{AABB, primitives::Rectangle};
use serde::{Deserialize, Serialize};
use crate::layout::LayerId;
use crate::layout::compounds::{ComponentId, NetId, PinId};
use crate::vector::Vector2;
use crate::{Rect3, Vector3, layout::LayerId};
use super::JointId;
@ -50,7 +49,7 @@ pub struct Via {
pub position: Vector2<i64>,
pub min_layer: LayerId,
pub max_layer: LayerId,
pub net: NetId,
pub net: Option<NetId>,
}
impl Via {
@ -59,20 +58,20 @@ impl Via {
<= self.spec.radius.pow(2)
}
pub fn bbox(&self) -> Rectangle<[i64; 3]> {
pub fn bbox(&self) -> Rect3<i64> {
let radius = self.spec.radius as i64;
Rectangle::from_aabb(AABB::from_corners(
[
Rect3::new(
Vector3::new(
self.position.x - radius,
self.position.y - radius,
self.min_layer.index() as i64,
],
[
),
Vector3::new(
self.position.x + radius,
self.position.y + radius,
self.max_layer.index() as i64,
],
))
),
)
}
}

View File

@ -24,6 +24,15 @@ impl Layout {
for &via_id in &component.vias {
self.update_via(via_id);
}
for &polygon_id in &component.polygons {
self.modify_polygon(polygon_id, |polygon| {
polygon
.vertices
.iter_mut()
.for_each(|vertex| *vertex += translation)
});
}
}
}
}

View File

@ -4,6 +4,7 @@
mod autorouter;
mod board;
mod compass;
mod drawer;
mod layout;
mod math;
@ -22,8 +23,8 @@ pub use crate::board::LayerDesc;
pub use crate::board::LayerSide;
pub use crate::board::LayerType;
pub use crate::board::interactors::{
DragSelectionInteractor, DragSelectionOptions, InteractiveInput, MasterInteractor,
SelectionCombineMode, SelectionContainMode, SelectionInteractor,
DragSelectInteractor, DragSelectOptions, MasterInteractor, SelectInteractor,
SelectionCombineMode, SelectionContainMode,
};
pub use crate::board::selections;
pub use crate::layout::LayerId;

View File

@ -51,8 +51,12 @@ impl Ratsnest {
BTreeMap::new();
for (i, joint) in board.layout().joints().container().iter() {
let Some(net) = joint.spec.net else {
continue;
};
let _ = triangulations
.entry((joint.spec.net, joint.spec.layer))
.entry((net, joint.spec.layer))
.or_insert_with(DelaunayTriangulation::new)
.insert(DelaunayVertex {
layer: joint.spec.layer,
@ -63,9 +67,13 @@ impl Ratsnest {
}
for (i, segment) in board.layout().segments().container().iter() {
let Some(net) = segment.net else {
continue;
};
let segment_center = segment.center();
let _ = triangulations
.entry((segment.net, segment.layer))
.entry((net, segment.layer))
.or_insert_with(DelaunayTriangulation::new)
.insert(DelaunayVertex {
layer: segment.layer,
@ -76,8 +84,12 @@ impl Ratsnest {
}
for (i, polygon) in board.layout().polygons().container().iter() {
let Some(net) = polygon.net else {
continue;
};
let _ = triangulations
.entry((polygon.net, polygon.layer))
.entry((net, polygon.layer))
.or_insert_with(DelaunayTriangulation::new)
.insert(DelaunayVertex {
layer: polygon.layer,

View File

@ -5,7 +5,7 @@
use derive_getters::Getters;
use derive_more::Constructor;
use num_traits::Bounded;
use rstar::{AABB, RTreeNum};
use rstar::{AABB, RTreeNum, primitives::Rectangle};
use serde::{Deserialize, Serialize};
use crate::{Vector2, Vector3};
@ -19,6 +19,36 @@ pub struct Rect2<T> {
}
impl<T: Copy + Ord> Rect2<T> {
pub fn union(self, other: Self) -> Self {
Self {
min: Vector2::new(
std::cmp::min(self.min.x, other.min.x),
std::cmp::min(self.min.y, other.min.y),
),
max: Vector2::new(
std::cmp::max(self.max.x, other.max.x),
std::cmp::max(self.max.y, other.max.y),
),
}
}
pub fn intersection(self, other: Self) -> Option<Self> {
let min = Vector2::new(
std::cmp::max(self.min.x, other.min.x),
std::cmp::max(self.min.y, other.min.y),
);
let max = Vector2::new(
std::cmp::min(self.max.x, other.max.x),
std::cmp::min(self.max.y, other.max.y),
);
if min.x > max.x || min.y > max.y {
return None;
}
Some(Self { min, max })
}
pub fn z_extruded(self, from: T, to: T) -> Rect3<T> {
Rect3 {
min: Vector3::new(self.min.x, self.min.y, std::cmp::min(from, to)),
@ -53,6 +83,32 @@ impl<T: RTreeNum> Rect2<T> {
}
}
macro_rules! impl_rect2_contains_point {
($type:ty) => {
impl Rect2<$type> {
pub fn contains_point(&self, point: Vector2<$type>) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
}
}
};
}
impl_rect2_contains_point!(i8);
impl_rect2_contains_point!(i16);
impl_rect2_contains_point!(i32);
impl_rect2_contains_point!(i64);
impl_rect2_contains_point!(i128);
impl_rect2_contains_point!(u8);
impl_rect2_contains_point!(u16);
impl_rect2_contains_point!(u32);
impl_rect2_contains_point!(u64);
impl_rect2_contains_point!(u128);
impl_rect2_contains_point!(f32);
impl_rect2_contains_point!(f64);
macro_rules! impl_rect2_contains_circle {
($type:ty) => {
impl Rect2<$type> {
@ -152,12 +208,15 @@ macro_rules! impl_rect2_intersects_polygon {
return false;
}
if self.contains_polygon(polygon) {
if polygon.iter().any(|&vertex| self.contains_point(vertex)) {
return true;
}
let corners = self.corners();
if corners.iter().any(|corner| corner.inside_polygon(polygon)) {
if self
.corners()
.iter()
.any(|corner| corner.inside_polygon(polygon))
{
return true;
}
@ -204,6 +263,40 @@ impl<T: Ord + Copy> Rect3<T> {
),
}
}
pub fn union(self, other: Self) -> Self {
Self {
min: Vector3::new(
std::cmp::min(self.min.x, other.min.x),
std::cmp::min(self.min.y, other.min.y),
std::cmp::min(self.min.z, other.min.z),
),
max: Vector3::new(
std::cmp::max(self.max.x, other.max.x),
std::cmp::max(self.max.y, other.max.y),
std::cmp::max(self.max.z, other.max.z),
),
}
}
pub fn intersection(self, other: Self) -> Option<Self> {
let min = Vector3::new(
std::cmp::max(self.min.x, other.min.x),
std::cmp::max(self.min.y, other.min.y),
std::cmp::max(self.min.z, other.min.z),
);
let max = Vector3::new(
std::cmp::min(self.max.x, other.max.x),
std::cmp::min(self.max.y, other.max.y),
std::cmp::min(self.max.z, other.max.z),
);
if min.x > max.x || min.y > max.y || min.z > max.z {
return None;
}
Some(Self { min, max })
}
}
impl<T: Copy + Ord> Rect3<T> {
@ -219,4 +312,8 @@ impl<T: RTreeNum> Rect3<T> {
[self.max.x, self.max.y, self.max.z],
)
}
pub fn rtree_rectangle(self) -> Rectangle<[T; 3]> {
Rectangle::from_aabb(self.aabb())
}
}

View File

@ -72,8 +72,6 @@ impl Board {
.chain(dsn.pcb.network.nets.iter().map(|net| &net.name))
.cloned()
.collect();
// deduplicate net names
tmp.push("outlines".to_string());
tmp.sort_unstable();
tmp.dedup();
@ -99,7 +97,6 @@ impl Board {
layer_descs,
net_names,
);
let outline_net = board.net_id("outlines").unwrap();
// Mapping of pin -> net prepared for adding pins.
let pin_nets: BTreeMap<String, NetId> = dsn
@ -152,7 +149,7 @@ impl Board {
&outline.path.coords,
outline.path.width,
outline_layer_id,
outline_net,
None,
Some(component_id),
None,
!place_side_is_front,
@ -162,10 +159,7 @@ impl Board {
for pin in &image.pins {
let pin_name = format!("{}-{}", place.name, pin.id);
let Some(net) = pin_nets.get(&pin_name).copied() else {
continue;
};
let net = pin_nets.get(&pin_name).copied();
let pin_id = board.ensure_named_pin(pin_name.clone());
let padstack = dsn.pcb.library.find_padstack_by_name(&pin.name).unwrap();
@ -261,7 +255,7 @@ impl Board {
PointWithRotation::default(),
circle.diameter / 2.0,
layer,
net,
Some(net),
None,
None,
false,
@ -279,7 +273,7 @@ impl Board {
rect.x2,
rect.y2,
layer,
net,
Some(net),
None,
None,
false,
@ -295,7 +289,7 @@ impl Board {
&path.coords,
path.width,
layer,
net,
Some(net),
None,
None,
false,
@ -311,7 +305,7 @@ impl Board {
&polygon.coords,
polygon.width,
layer,
net,
Some(net),
None,
None,
false,
@ -333,7 +327,7 @@ impl Board {
&wire.path.coords,
wire.path.width,
layer,
net,
Some(net),
None,
None,
false,
@ -350,7 +344,7 @@ impl Board {
pin_pos: PointWithRotation,
radius: f64,
layer: LayerId,
net: NetId,
net: Option<NetId>,
component: Option<ComponentId>,
pin: Option<PinId>,
flip: bool,
@ -375,7 +369,7 @@ impl Board {
x2: f64,
y2: f64,
layer: LayerId,
net: NetId,
net: Option<NetId>,
component: Option<ComponentId>,
pin: Option<PinId>,
flip: bool,
@ -402,7 +396,7 @@ impl Board {
coords: &[Point],
width: f64,
layer: LayerId,
net: NetId,
net: Option<NetId>,
component: Option<ComponentId>,
pin: Option<PinId>,
flip: bool,
@ -465,7 +459,7 @@ impl Board {
coords: &[Point],
_width: f64,
layer: LayerId,
net: NetId,
net: Option<NetId>,
component: Option<ComponentId>,
pin: Option<PinId>,
flip: bool,

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use undoredo::FlushDelta;
use crate::{Autorouter, Board, selections::PersistableSelection};
pub enum Workspace {
@ -10,7 +12,9 @@ pub enum Workspace {
}
impl Workspace {
pub fn new_board(board: Board) -> Self {
pub fn new_board(mut board: Board) -> Self {
board.flush_delta();
Self::Board(BoardWorkspace::new(board))
}