Add functional Undo/Redo buttons

This commit is contained in:
Mikolaj Wielgus 2026-06-08 16:48:58 +02:00
parent 2cc4c52a59
commit ffa94cff82
15 changed files with 227 additions and 223 deletions

View File

@ -12,6 +12,7 @@ use crate::{
pub struct Actions {
pub file: FileActions,
pub edit: EditActions,
pub run: RunActions,
pub debug: DebugActions,
}
@ -20,6 +21,7 @@ impl Actions {
pub fn new(tr: &Translator) -> Self {
Self {
file: FileActions::new(tr),
edit: EditActions::new(tr),
run: RunActions::new(tr),
debug: DebugActions::new(tr),
}
@ -76,6 +78,48 @@ impl FileActions {
}
}
pub struct EditActions {
pub undo: Trigger,
pub redo: Trigger,
}
impl EditActions {
pub fn new(tr: &Translator) -> Self {
Self {
undo: Action::new(
tr.text("tr-menu-edit-undo"),
egui::Modifiers::CTRL,
egui::Key::Z,
)
.into_trigger(),
redo: Action::new(
tr.text("tr-menu-edit-redo"),
egui::Modifiers::CTRL,
egui::Key::Y,
)
.into_trigger(),
}
}
pub fn render_menu(
&mut self,
ctx: &Context,
ui: &mut Ui,
have_workspace: bool,
can_undo: bool,
can_redo: bool,
) {
ui.add_enabled_ui(have_workspace, |ui| {
ui.add_enabled_ui(can_undo, |ui| {
self.undo.button(ctx, ui);
});
ui.add_enabled_ui(can_redo, |ui| {
self.redo.button(ctx, ui);
});
});
}
}
pub struct RunActions {
pub autoplace: Trigger,
}

View File

@ -18,7 +18,7 @@ pub struct Controller {
impl Controller {
pub fn new(board: Board, tr: &Translator) -> Self {
let appearance_panel = LayersPanel::new(&board);
let workspace = Workspace::new_board(board);
let workspace = Workspace::new(board);
Self {
master_interactor: MasterInteractor::new(workspace.selection().clone()),
@ -95,15 +95,13 @@ impl Controller {
None
};
if let Workspace::Board(workspace) = &mut self.workspace {
self.master_interactor.abort(&mut workspace.board);
self.master_interactor.abort(self.workspace.board_mut());
if let Some(board_master) = board_master {
self.master_interactor = MasterInteractor::Board(board_master);
}
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
if let Some(board_master) = board_master {
self.master_interactor = MasterInteractor::Board(board_master);
}
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
}
let primary_pressed = ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary));
@ -123,47 +121,44 @@ impl Controller {
self.master_interactor = MasterInteractor::new(self.workspace.selection().clone());
}
if let Workspace::Board(workspace) = &mut self.workspace {
if primary_down {
self.master_interactor.hold(
&mut workspace.board,
self.appearance_panel.active,
pointer_on_scene,
if primary_down {
self.master_interactor.hold(
self.workspace.board_mut(),
self.appearance_panel.active,
pointer_on_scene,
);
if let Some(select_interactor) = self.master_interactor.select_interactor().as_ref()
{
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,
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,
),
);
if let Some(select_interactor) =
self.master_interactor.select_interactor().as_ref()
{
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,
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)
};
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,
);
}
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,
);
}
}
}
@ -177,18 +172,22 @@ impl Controller {
.map(|select_interactor| *select_interactor.origin())
.unwrap_or(Vector2::new(0, 0))
});
if let Workspace::Board(workspace) = &mut self.workspace {
self.master_interactor
.release(&mut workspace.board, active, pointer_for_scene);
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
if self
.master_interactor
.release(self.workspace.board_mut(), active, pointer_for_scene)
.is_break()
{
self.workspace.commit();
}
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
}
if delete_pressed {
if let Workspace::Board(workspace) = &mut self.workspace {
self.master_interactor.delete(&mut workspace.board);
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
}
self.master_interactor.delete(self.workspace.board_mut());
self.workspace.commit();
*self.workspace.selection_mut() = self.master_interactor.selection().clone();
}
}
}

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{controller::Controller, viewport::Viewport};
use topola::{Orientation, Vector2, Workspace};
use topola::{Orientation, Vector2};
pub struct DebugOverlay {}
@ -267,72 +267,10 @@ impl DebugOverlay {
fn display_navmeshes(
&mut self,
ctx: &egui::Context,
ui: &egui::Ui,
viewport: &Viewport,
workspace: &Controller,
_ctx: &egui::Context,
_ui: &egui::Ui,
_viewport: &Viewport,
_workspace: &Controller,
) {
crate::profile_function!();
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
return;
};
let autorouter = &autorouter_workspace.autorouter;
for layer in workspace
.appearance_panel
.layers_in_display_order(*workspace.workspace.board().layout().layer_count())
{
if workspace.appearance_panel.visible[layer.index()] {
for navmesh in autorouter
.router()
.navmesher_board()
.navmesher()
.layer_navmeshers()[layer.index()]
.navmeshes()
{
for edge_geom in navmesh
.triangulation()
.rtreed_dcel()
.edges_rtree()
.as_ref()
.iter()
{
let (from_vertex, to_vertex) = navmesh
.triangulation()
.rtreed_dcel()
.dcel()
.edge_endpoints(edge_geom.data);
let from = navmesh
.triangulation()
.rtreed_dcel()
.dcel()
.vertex_weight(from_vertex)
.position();
let to = navmesh
.triangulation()
.rtreed_dcel()
.dcel()
.vertex_weight(to_vertex)
.position();
ui.painter().line_segment(
[
egui::pos2(*from.x() as f32, *from.y() as f32),
egui::pos2(*to.x() as f32, *to.y() as f32),
],
egui::Stroke::new(
10.0,
egui::Color32::WHITE,
/*workspace
.appearance_panel
.colors(ctx)
.layers
.color(workspace.autorouter.navmesher_board().board().layer_name(layer))
.normal,*/
),
);
}
}
}
}
}
}

View File

@ -8,10 +8,7 @@ use crate::{
menu_bar::MenuBar,
viewport::Viewport,
};
use topola::{
Workspace,
layout::primitives::{Joint, Poly, Seg, Via},
};
use topola::layout::primitives::{Joint, Poly, Seg, Via};
pub struct Display {}
@ -252,12 +249,8 @@ impl Display {
workspace: &Controller,
) {
crate::profile_function!();
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
return;
};
let autorouter = &autorouter_workspace.autorouter;
for ratline in autorouter.ratsnest().ratlines() {
for ratline in workspace.workspace.ratsnest().ratlines() {
let layers = *ratline.endpoint_layers();
let endpoints = *ratline.endpoints();
@ -267,8 +260,6 @@ impl Display {
continue;
}
//let stroke_width = 2.0 / viewport.scale_factor().max(1e-6);
ui.painter().line_segment(
[
egui::pos2(endpoints[0].x as f32, endpoints[0].y as f32),

View File

@ -65,6 +65,14 @@ impl MenuBar {
crate::profile_function!();
let mut actions = Actions::new(tr);
let mut controller = controller;
let can_undo = controller
.as_ref()
.is_some_and(|controller| !controller.workspace.history().done().is_empty());
let can_redo = controller
.as_ref()
.is_some_and(|controller| !controller.workspace.history().undone().is_empty());
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
@ -74,6 +82,14 @@ impl MenuBar {
ui.separator();
//ui.menu_button(tr.text("tr-menu-edit"), |ui| {
actions
.edit
.render_menu(ctx, ui, controller.is_some(), can_undo, can_redo);
//});
ui.separator();
actions.run.render_menu(ctx, ui, controller.is_some());
ui.separator();
@ -132,8 +148,20 @@ impl MenuBar {
});
}
if can_undo && actions.edit.undo.consume_key_triggered(ctx, ui) {
if let Some(controller) = controller.as_mut() {
controller.workspace.undo();
}
}
if can_redo && actions.edit.redo.consume_key_triggered(ctx, ui) {
if let Some(controller) = controller.as_mut() {
controller.workspace.redo();
}
}
if actions.run.autoplace.consume_key_triggered(ctx, ui) {
if let Some(controller) = controller {
if let Some(controller) = controller.as_mut() {
controller.master_interactor.autoplace(
controller.workspace.board_mut(),
AutoplacerSchedule {

View File

@ -57,8 +57,8 @@ impl Interactor for AutoplacerMasterInteractor {
self.board_master.hold(board, layer, pointer);
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
self.board_master.release(board, layer, pointer);
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
self.board_master.release(board, layer, pointer)
}
fn abort(&mut self, board: &mut Board) {

View File

@ -4,21 +4,17 @@
use derive_getters::Getters;
use crate::{board::Board, ratsnest::Ratsnest, router::Router};
use crate::{board::Board, router::Router};
#[derive(Clone, Debug, Getters)]
pub struct Autorouter {
ratsnest: Ratsnest,
router: Router,
}
impl Autorouter {
pub fn new(board: Board) -> Self {
let ratsnest = Ratsnest::new(&board);
Self {
router: Router::new(board),
ratsnest,
}
}
}

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::ops::ControlFlow;
use derive_getters::Getters;
use derive_more::Constructor;
use undoredo::ResetDelta;
@ -25,8 +27,9 @@ impl Interactor for DragMoveInteractor {
board.move_components_by(self.selection.clone(), pointer - self.origin);
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
self.hold(board, layer, pointer);
ControlFlow::Break(())
}
fn abort(&mut self, board: &mut Board) {

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::ops::ControlFlow;
use derive_getters::Getters;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};
@ -185,8 +187,9 @@ impl Interactor for DragSelectInteractor {
self.selection = combined_selection;
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
self.hold(board, layer, pointer);
ControlFlow::Continue(())
}
fn abort(&mut self, _board: &mut Board) {

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::ops::ControlFlow;
use derive_getters::Getters;
use crate::{
@ -62,16 +64,21 @@ impl Interactor for BoardMasterInteractor {
}
}
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, layer, pointer);
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
let commit = if let Some(drag_move_interactor) = self.drag_move_interactor.as_mut() {
drag_move_interactor.release(board, layer, pointer)
} else if let Some(select_interactor) = self.select_interactor.as_mut() {
select_interactor.release(board, layer, pointer);
self.selection = select_interactor.selection().clone();
}
ControlFlow::Continue(())
} else {
ControlFlow::Continue(())
};
self.select_interactor = None;
self.drag_move_interactor = None;
commit
}
fn abort(&mut self, board: &mut Board) {

View File

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::ops::ControlFlow;
use derive_getters::Getters;
use crate::{
@ -56,7 +58,7 @@ impl Interactor for SelectInteractor {
self.selection = drag_selection_interactor.selection().clone();
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
if pointer == self.origin {
let mut selection = self.original_selection.clone();
let point = Vector3::new(pointer.x, pointer.y, layer.index() as i64);
@ -76,10 +78,11 @@ impl Interactor for SelectInteractor {
}
self.selection = selection;
return;
return ControlFlow::Continue(());
}
self.hold(board, layer, pointer);
ControlFlow::Continue(())
}
fn abort(&mut self, _board: &mut Board) {

View File

@ -19,10 +19,19 @@ pub trait Interactor {
fn step(&mut self, _board: &mut Board) -> ControlFlow<()> {
ControlFlow::Continue(())
}
fn delete(&mut self, board: &mut Board) {}
fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {}
fn abort(&mut self, board: &mut Board) {}
fn delete(&mut self, board: &mut Board) {
let _ = board;
}
fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
let _ = (board, layer, pointer);
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
let _ = (board, layer, pointer);
ControlFlow::Continue(())
}
fn abort(&mut self, board: &mut Board) {
let _ = board;
}
}
pub enum MasterInteractor {
@ -47,7 +56,6 @@ impl MasterInteractor {
));
}
_ => (),
//_ => panic!("autoplacement can be only started from board at rest"),
}
}
@ -94,7 +102,7 @@ impl Interactor for MasterInteractor {
}
}
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) -> ControlFlow<()> {
match self {
Self::Board(interactor) => interactor.release(board, layer, pointer),
Self::Autoplacer(interactor) => interactor.release(board, layer, pointer),

View File

@ -22,6 +22,7 @@ mod specctra;
mod vector;
mod workspace;
pub use crate::board::BoardDelta;
pub use crate::autoplacer::AutoplacerSchedule;
pub use crate::autorouter::Autorouter;
pub use crate::board::selections;
@ -30,4 +31,4 @@ pub use crate::orientation::Orientation;
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};
pub use crate::workspace::Workspace;

View File

@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize};
use spade::{DelaunayTriangulation, HasPosition, Triangulation, handles::FixedVertexHandle};
use crate::{
board::Board,
layout::{
LayerId,
compounds::NetId,
primitives::{JointId, PolyId, PrimitiveId, SegId},
Layout,
},
vector::Vector2,
};
@ -40,19 +40,19 @@ impl HasPosition for DelaunayVertex {
}
}
#[derive(Clone, Debug, Deserialize, Getters, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Getters, Serialize)]
pub struct Ratsnest {
ratlines: Vec<Ratline>,
}
impl Ratsnest {
pub fn new(board: &Board) -> Self {
pub fn new(layout: &Layout) -> Self {
let mut ratlines = Vec::new();
let mut triangulations: BTreeMap<(NetId, LayerId), DelaunayTriangulation<DelaunayVertex>> =
BTreeMap::new();
for (i, joint) in board.layout().joints().container().iter() {
for (i, joint) in layout.joints().container().iter() {
let Some(net) = joint.spec.net else {
continue;
};
@ -68,7 +68,7 @@ impl Ratsnest {
});
}
for (i, seg) in board.layout().segs().container().iter() {
for (i, seg) in layout.segs().container().iter() {
let Some(net) = seg.net else {
continue;
};
@ -85,7 +85,7 @@ impl Ratsnest {
});
}
for (i, poly) in board.layout().polys().container().iter() {
for (i, poly) in layout.polys().container().iter() {
let Some(net) = poly.spec.net else {
continue;
};

View File

@ -2,84 +2,67 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use undoredo::FlushDelta;
use derive_getters::Getters;
use undoredo::{FlushDelta, UndoRedo};
use crate::{autorouter::Autorouter, board::Board, selections::PersistableSelection};
use crate::{
board::{Board, BoardDelta, selections::PersistableSelection},
ratsnest::Ratsnest,
};
pub enum Workspace {
Board(BoardWorkspace),
Autorouter(AutorouterWorkspace),
#[derive(Getters)]
pub struct Workspace {
board: Board,
selection: PersistableSelection,
history: UndoRedo<BoardDelta>,
ratsnest: Ratsnest,
}
impl Workspace {
pub fn new_board(mut board: Board) -> Self {
pub fn new(mut board: Board) -> Self {
board.flush_delta();
let ratsnest = Ratsnest::new(board.layout());
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()
}
Self {
board,
selection: PersistableSelection::new(),
history: UndoRedo::new(),
ratsnest,
}
}
pub fn board_mut(&mut self) -> &mut Board {
match self {
Workspace::Board(workspace) => &mut workspace.board,
Workspace::Autorouter(_workspace) => todo!(),
/*Workspace::Autorouter(workspace) => {
workspace.autorouter.router().navmesher_board().board()
}*/
&mut self.board
}
pub fn selection_mut(&mut self) -> &mut PersistableSelection {
&mut self.selection
}
pub fn commit(&mut self) {
self.history.commit(&mut self.board);
self.rebuild_ratsnest();
}
pub fn undo(&mut self) -> bool {
if self.history.undo(&mut self.board).is_some() {
self.rebuild_ratsnest();
true
} else {
false
}
}
}
pub struct BoardWorkspace {
pub board: Board,
pub selection: PersistableSelection,
}
impl BoardWorkspace {
pub fn new(board: Board) -> Self {
Self {
board,
selection: PersistableSelection::new(),
pub fn redo(&mut self) -> bool {
if self.history.redo(&mut self.board).is_some() {
self.rebuild_ratsnest();
true
} else {
false
}
}
}
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(),
}
fn rebuild_ratsnest(&mut self) {
self.ratsnest = Ratsnest::new(self.board.layout());
}
}