mirror of https://codeberg.org/topola/topola.git
Move some GUI logic and workspace to new `Controller` struct
This commit is contained in:
parent
b2ef23c06c
commit
65f96f1fc2
|
|
@ -9,7 +9,7 @@ use topola::board::Board;
|
|||
use unic_langid::langid;
|
||||
|
||||
use crate::{
|
||||
menu_bar::MenuBar, translator::Translator, viewport::Viewport, workspace::GuiWorkspace,
|
||||
controller::Controller, menu_bar::MenuBar, translator::Translator, viewport::Viewport,
|
||||
};
|
||||
|
||||
pub struct App {
|
||||
|
|
@ -22,7 +22,7 @@ pub struct App {
|
|||
|
||||
menu_bar: MenuBar,
|
||||
viewport: Viewport,
|
||||
workspace: Option<GuiWorkspace>,
|
||||
controller: Option<Controller>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
|
|
@ -32,7 +32,7 @@ impl Default for App {
|
|||
content_channel: channel(),
|
||||
menu_bar: MenuBar::new(),
|
||||
viewport: Viewport::new(),
|
||||
workspace: None,
|
||||
controller: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ impl App {
|
|||
|
||||
fn update_state(&mut self) {
|
||||
if let Ok(data) = self.content_channel.1.try_recv() {
|
||||
self.workspace = Some(GuiWorkspace::new(
|
||||
self.controller = Some(Controller::new(
|
||||
Board::from_specctra(data.unwrap()),
|
||||
&self.translator,
|
||||
));
|
||||
|
|
@ -130,15 +130,15 @@ impl eframe::App for App {
|
|||
|
||||
self.update_state();
|
||||
|
||||
if let Some(ref mut workspace) = self.workspace {
|
||||
workspace.update_appearance_panel(ctx);
|
||||
if let Some(ref mut controller) = self.controller {
|
||||
controller.update_appearance_panel(ctx);
|
||||
}
|
||||
|
||||
self.viewport.update(
|
||||
&self.translator,
|
||||
ctx,
|
||||
&self.menu_bar,
|
||||
self.workspace.as_mut(),
|
||||
self.controller.as_mut(),
|
||||
);
|
||||
|
||||
self.update_locale();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::{ops::ControlFlow, time::Instant};
|
||||
|
||||
use topola::{Interactor, MasterInteractor, Vector2, Workspace, board::Board};
|
||||
|
||||
use crate::{layers_panel::LayersPanel, translator::Translator};
|
||||
|
||||
pub struct Controller {
|
||||
pub workspace: Workspace,
|
||||
pub appearance_panel: LayersPanel,
|
||||
pub master_interactor: Option<MasterInteractor>,
|
||||
pub dt_accum: f64,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(board: Board, tr: &Translator) -> Self {
|
||||
let appearance_panel = LayersPanel::new(&board);
|
||||
|
||||
Self {
|
||||
workspace: Workspace::new_board(board),
|
||||
appearance_panel,
|
||||
master_interactor: None,
|
||||
dt_accum: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_state_by_dt(
|
||||
&mut self,
|
||||
tr: &Translator,
|
||||
step_rate: Option<f64>,
|
||||
dt: f64,
|
||||
) -> bool {
|
||||
let instant = Instant::now();
|
||||
|
||||
if step_rate.is_some() {
|
||||
self.dt_accum += dt;
|
||||
}
|
||||
|
||||
while step_rate.is_none_or(|step_rate| self.dt_accum >= 1.0 / step_rate) {
|
||||
if let Some(step_rate) = step_rate {
|
||||
self.dt_accum -= 1.0 / step_rate;
|
||||
}
|
||||
|
||||
if let ControlFlow::Break(()) = self.step(tr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hard limit: never spend more time on advancing state than the
|
||||
// duration of last frame to prevent stuttering.
|
||||
// Of course, this does not safeguard against infinite loops.
|
||||
if instant.elapsed().as_secs_f64() >= dt {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn step(&mut self, tr: &Translator) -> ControlFlow<()> {
|
||||
self.master_interactor
|
||||
.as_mut()
|
||||
.map(|master_interactor| master_interactor.step(self.workspace.board_mut()));
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
|
||||
pub fn update_appearance_panel(&mut self, ctx: &egui::Context) {
|
||||
let Self {
|
||||
workspace,
|
||||
appearance_panel,
|
||||
master_interactor: _,
|
||||
dt_accum,
|
||||
} = self;
|
||||
|
||||
appearance_panel.update(ctx, workspace.board_mut());
|
||||
}
|
||||
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
tr: &Translator,
|
||||
ctx: &egui::Context,
|
||||
step_rate: Option<f64>,
|
||||
scene_to_viewport: egui::emath::TSTransform,
|
||||
scene_hovered: bool,
|
||||
ui: &mut egui::Ui,
|
||||
) {
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
if let (Some(interactor), Workspace::Board(workspace)) =
|
||||
(&mut self.master_interactor, &mut self.workspace)
|
||||
{
|
||||
interactor.abort(&mut workspace.board);
|
||||
*self.workspace.selection_mut() = interactor.selection().clone();
|
||||
}
|
||||
self.master_interactor = None;
|
||||
}
|
||||
|
||||
let primary_pressed = ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary));
|
||||
let primary_down = 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()) {
|
||||
let pointer_on_scene_pos = scene_to_viewport.inverse() * pointer_viewport_pos;
|
||||
let pointer_on_scene =
|
||||
Vector2::new(pointer_on_scene_pos.x as i64, pointer_on_scene_pos.y as i64);
|
||||
maybe_pointer_on_scene = Some(pointer_on_scene);
|
||||
|
||||
if primary_pressed && scene_hovered {
|
||||
self.master_interactor =
|
||||
Some(MasterInteractor::new(self.workspace.selection().clone()));
|
||||
}
|
||||
|
||||
if let (Some(interactor), Workspace::Board(workspace)) =
|
||||
(&mut self.master_interactor, &mut self.workspace)
|
||||
{
|
||||
if primary_down {
|
||||
interactor.hold(
|
||||
&mut workspace.board,
|
||||
self.appearance_panel.active,
|
||||
pointer_on_scene,
|
||||
);
|
||||
|
||||
if let Some(select_interactor) = 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)
|
||||
};
|
||||
|
||||
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.master_interactor.take() {
|
||||
let active = self.appearance_panel.active;
|
||||
let pointer_for_scene = maybe_pointer_on_scene.unwrap_or_else(|| {
|
||||
interactor
|
||||
.select_interactor()
|
||||
.as_ref()
|
||||
.map(|select_interactor| *select_interactor.origin())
|
||||
.unwrap_or(Vector2::new(0, 0))
|
||||
});
|
||||
if let Workspace::Board(workspace) = &mut self.workspace {
|
||||
interactor.release(&mut workspace.board, active, pointer_for_scene);
|
||||
*self.workspace.selection_mut() = interactor.selection().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if delete_pressed {
|
||||
let mut interactor = MasterInteractor::new(self.workspace.selection().clone());
|
||||
if let Workspace::Board(workspace) = &mut self.workspace {
|
||||
interactor.delete(&mut workspace.board);
|
||||
*self.workspace.selection_mut() = interactor.selection().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{viewport::Viewport, workspace::GuiWorkspace};
|
||||
use crate::{controller::Controller, viewport::Viewport};
|
||||
use topola::{
|
||||
Orientation, Vector2, Workspace,
|
||||
layout::primitives::{Joint, Polygon, Segment, Via},
|
||||
|
|
@ -21,7 +21,7 @@ impl Display {
|
|||
ui: &egui::Ui,
|
||||
//menu_bar: &MenuBar,
|
||||
viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
workspace: &Controller,
|
||||
) {
|
||||
self.display_layout(ctx, ui, /*menu_bar,*/ viewport, workspace);
|
||||
self.display_repulsions(ui, viewport, workspace);
|
||||
|
|
@ -37,7 +37,7 @@ impl Display {
|
|||
ui: &egui::Ui,
|
||||
//menu_bar: &MenuBar,
|
||||
viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
workspace: &Controller,
|
||||
) {
|
||||
let board = workspace.workspace.board();
|
||||
let layout = board.layout();
|
||||
|
|
@ -160,7 +160,7 @@ impl Display {
|
|||
);
|
||||
}
|
||||
|
||||
fn display_repulsions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &GuiWorkspace) {
|
||||
fn display_repulsions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &Controller) {
|
||||
let board = workspace.workspace.board();
|
||||
let stroke = egui::Stroke::new(150.0 / viewport.scale_factor(), egui::Color32::YELLOW);
|
||||
|
||||
|
|
@ -201,12 +201,7 @@ impl Display {
|
|||
}
|
||||
}
|
||||
|
||||
fn display_attractions(
|
||||
&mut self,
|
||||
ui: &egui::Ui,
|
||||
viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
) {
|
||||
fn display_attractions(&mut self, ui: &egui::Ui, viewport: &Viewport, workspace: &Controller) {
|
||||
let board = workspace.workspace.board();
|
||||
let layout = board.layout();
|
||||
let stroke = egui::Stroke::new(150.0 / viewport.scale_factor(), egui::Color32::BLUE);
|
||||
|
|
@ -336,7 +331,7 @@ impl Display {
|
|||
ctx: &egui::Context,
|
||||
ui: &egui::Ui,
|
||||
viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
workspace: &Controller,
|
||||
) {
|
||||
let board = workspace.workspace.board();
|
||||
let layout = board.layout();
|
||||
|
|
@ -413,7 +408,7 @@ impl Display {
|
|||
ctx: &egui::Context,
|
||||
ui: &egui::Ui,
|
||||
viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
workspace: &Controller,
|
||||
) {
|
||||
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
|
||||
return;
|
||||
|
|
@ -483,7 +478,7 @@ impl Display {
|
|||
_ctx: &egui::Context,
|
||||
ui: &egui::Ui,
|
||||
_viewport: &Viewport,
|
||||
workspace: &GuiWorkspace,
|
||||
workspace: &Controller,
|
||||
) {
|
||||
let Workspace::Autorouter(autorouter_workspace) = &workspace.workspace else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
mod action;
|
||||
mod actions;
|
||||
mod app;
|
||||
mod controller;
|
||||
mod display;
|
||||
mod layers_panel;
|
||||
mod menu_bar;
|
||||
mod translator;
|
||||
mod viewport;
|
||||
mod workspace;
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use egui::Pos2;
|
||||
use topola::{Interactor, MasterInteractor, Vector2, Workspace};
|
||||
|
||||
use crate::{display::Display, menu_bar::MenuBar, translator::Translator, workspace::GuiWorkspace};
|
||||
use crate::{controller::Controller, display::Display, menu_bar::MenuBar, translator::Translator};
|
||||
|
||||
pub struct Viewport {
|
||||
pub scene_rect: egui::Rect,
|
||||
pub ref_scene_rect: egui::Rect,
|
||||
pub scheduled_zoom_to_fit: bool,
|
||||
master_interactor: Option<MasterInteractor>,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
|
|
@ -20,7 +18,6 @@ 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,
|
||||
master_interactor: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +26,7 @@ impl Viewport {
|
|||
tr: &Translator,
|
||||
ctx: &egui::Context,
|
||||
menu_bar: &MenuBar,
|
||||
workspace: Option<&mut GuiWorkspace>,
|
||||
controller: Option<&mut Controller>,
|
||||
) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
|
|
@ -44,9 +41,9 @@ impl Viewport {
|
|||
.zoom_range(zoom_range.clone())
|
||||
.drag_pan_buttons(egui::DragPanButtons::MIDDLE)
|
||||
.show(ui, &mut scene_rect, |ui| {
|
||||
if let Some(ref workspace) = workspace {
|
||||
if let Some(ref controller) = controller {
|
||||
let mut display = Display::new();
|
||||
display.update(ctx, ui, &self, workspace);
|
||||
display.update(ctx, ui, &self, controller);
|
||||
}
|
||||
})
|
||||
.response;
|
||||
|
|
@ -56,143 +53,29 @@ impl Viewport {
|
|||
let scene_to_viewport =
|
||||
Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into());
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
workspace.advance_state_by_dt(
|
||||
if let Some(controller) = controller {
|
||||
controller.advance_state_by_dt(
|
||||
tr,
|
||||
menu_bar.fix_step_rate.then_some(menu_bar.step_rate),
|
||||
ctx.input(|i| {
|
||||
if i.stable_dt <= i.predicted_dt {
|
||||
i.stable_dt
|
||||
} else {
|
||||
// Clamp dt to egui's predicted dt to
|
||||
// additionally safeguard against stuttering.
|
||||
i.predicted_dt
|
||||
}
|
||||
}) as f64,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
controller.handle_input(
|
||||
tr,
|
||||
ctx,
|
||||
menu_bar.fix_step_rate.then_some(menu_bar.step_rate),
|
||||
scene_to_viewport,
|
||||
response.hovered(),
|
||||
ui,
|
||||
);
|
||||
|
||||
let primary_pressed =
|
||||
ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Primary));
|
||||
let primary_down =
|
||||
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()) {
|
||||
let pointer_on_scene_pos =
|
||||
scene_to_viewport.inverse() * pointer_viewport_pos;
|
||||
let pointer_on_scene = Vector2::new(
|
||||
pointer_on_scene_pos.x as i64,
|
||||
pointer_on_scene_pos.y as i64,
|
||||
);
|
||||
maybe_pointer_on_scene = Some(pointer_on_scene);
|
||||
|
||||
if primary_pressed && response.hovered() {
|
||||
self.master_interactor = Some(MasterInteractor::new(
|
||||
workspace.workspace.selection().clone(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(interactor) = self.master_interactor.as_mut() {
|
||||
if primary_down {
|
||||
let board = match &mut workspace.workspace {
|
||||
Workspace::Board(workspace) => &mut workspace.board,
|
||||
Workspace::Autorouter(_) => panic!("expected board workspace"),
|
||||
};
|
||||
interactor.hold(
|
||||
board,
|
||||
workspace.appearance_panel.active,
|
||||
pointer_on_scene,
|
||||
);
|
||||
|
||||
if let Some(select_interactor) =
|
||||
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)
|
||||
};
|
||||
|
||||
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.master_interactor.take() {
|
||||
let pointer_for_scene = maybe_pointer_on_scene.unwrap_or_else(|| {
|
||||
interactor
|
||||
.select_interactor()
|
||||
.as_ref()
|
||||
.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.release(
|
||||
board,
|
||||
workspace.appearance_panel.active,
|
||||
pointer_for_scene,
|
||||
);
|
||||
|
||||
*workspace.workspace.selection_mut() = interactor.selection().clone();
|
||||
}
|
||||
}
|
||||
|
||||
if delete_pressed {
|
||||
let mut interactor =
|
||||
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.delete(board);
|
||||
*workspace.workspace.selection_mut() = interactor.selection().clone();
|
||||
}
|
||||
|
||||
self.zoom_to_fit_if_scheduled(workspace);
|
||||
self.zoom_to_fit_if_scheduled(controller);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
@ -210,7 +93,8 @@ impl Viewport {
|
|||
rect_in_scene: egui::Rect,
|
||||
zoom_range: egui::Rangef,
|
||||
) -> egui::emath::TSTransform {
|
||||
// Compute the scale factor to fit the bounding rectangle into the available screen size:
|
||||
// Compute the scale factor to fit the bounding rect into the available
|
||||
// screen size:
|
||||
let scale = rect_in_viewport.size() / rect_in_scene.size();
|
||||
|
||||
// Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
|
||||
|
|
@ -228,24 +112,24 @@ impl Viewport {
|
|||
* egui::emath::TSTransform::from_scaling(scale)
|
||||
}
|
||||
|
||||
fn zoom_to_fit_if_scheduled(&mut self, workspace: &GuiWorkspace) {
|
||||
fn zoom_to_fit_if_scheduled(&mut self, controller: &Controller) {
|
||||
if self.scheduled_zoom_to_fit {
|
||||
self.scene_rect = Self::boundary_bounding_box(workspace);
|
||||
self.scene_rect = Self::boundary_bounding_box(controller);
|
||||
self.ref_scene_rect = self.scene_rect.clone();
|
||||
}
|
||||
|
||||
self.scheduled_zoom_to_fit = false;
|
||||
}
|
||||
|
||||
fn boundary_bounding_box(workspace: &GuiWorkspace) -> egui::Rect {
|
||||
let first = workspace.workspace.board().layout().boundary()[0];
|
||||
fn boundary_bounding_box(controller: &Controller) -> egui::Rect {
|
||||
let first = controller.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.workspace.board().layout().boundary()[1..].iter() {
|
||||
for point in controller.workspace.board().layout().boundary()[1..].iter() {
|
||||
if point[0] < min_x {
|
||||
min_x = point[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::{ops::ControlFlow, time::Instant};
|
||||
|
||||
use topola::{Workspace, board::Board};
|
||||
|
||||
use crate::{layers_panel::LayersPanel, translator::Translator};
|
||||
|
||||
pub struct GuiWorkspace {
|
||||
pub workspace: Workspace,
|
||||
pub appearance_panel: LayersPanel,
|
||||
pub dt_accum: f64,
|
||||
}
|
||||
|
||||
impl GuiWorkspace {
|
||||
pub fn new(board: Board, tr: &Translator) -> Self {
|
||||
let appearance_panel = LayersPanel::new(&board);
|
||||
|
||||
Self {
|
||||
workspace: Workspace::new_board(board),
|
||||
appearance_panel,
|
||||
dt_accum: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_state_by_dt(
|
||||
&mut self,
|
||||
tr: &Translator,
|
||||
step_rate: Option<f64>,
|
||||
dt: f64,
|
||||
) -> bool {
|
||||
let instant = Instant::now();
|
||||
|
||||
if step_rate.is_some() {
|
||||
self.dt_accum += dt;
|
||||
}
|
||||
|
||||
while step_rate.is_none_or(|step_rate| self.dt_accum >= 1.0 / step_rate) {
|
||||
if let Some(step_rate) = step_rate {
|
||||
self.dt_accum -= 1.0 / step_rate;
|
||||
}
|
||||
|
||||
if let ControlFlow::Break(()) = self.step(tr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hard limit: never spend more time on advancing state than the
|
||||
// duration of last frame to prevent stuttering.
|
||||
// Of course, this does not safeguard against infinite loops.
|
||||
if instant.elapsed().as_secs_f64() >= dt {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn step(&mut self, tr: &Translator) -> ControlFlow<()> {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
|
||||
pub fn update_appearance_panel(&mut self, ctx: &egui::Context) {
|
||||
let Self {
|
||||
workspace,
|
||||
appearance_panel,
|
||||
dt_accum,
|
||||
} = self;
|
||||
|
||||
appearance_panel.update(ctx, workspace.board());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
autoplacer::{Autoplacer, AutoplacerSchedule},
|
||||
board::{
|
||||
Board,
|
||||
interactors::{MasterInteractor as BoardMasterInteractor, SelectInteractor},
|
||||
selections::PersistableSelection,
|
||||
},
|
||||
interactor::Interactor,
|
||||
layout::LayerId,
|
||||
vector::Vector2,
|
||||
};
|
||||
|
||||
pub struct MasterInteractor {
|
||||
board_master: BoardMasterInteractor,
|
||||
autoplacer: Autoplacer,
|
||||
}
|
||||
|
||||
impl MasterInteractor {
|
||||
pub fn new(
|
||||
board: &mut Board,
|
||||
selection: PersistableSelection,
|
||||
schedule: AutoplacerSchedule,
|
||||
) -> Self {
|
||||
Self {
|
||||
board_master: BoardMasterInteractor::new(selection.clone()),
|
||||
autoplacer: Autoplacer::new(board, selection.components.clone(), schedule),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selection(&self) -> &PersistableSelection {
|
||||
self.board_master.selection()
|
||||
}
|
||||
|
||||
pub fn select_interactor(&self) -> &Option<SelectInteractor> {
|
||||
self.board_master.select_interactor()
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactor for MasterInteractor {
|
||||
fn step(&mut self, board: &mut Board) {
|
||||
self.autoplacer.step(board);
|
||||
}
|
||||
|
||||
fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
|
||||
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 abort(&mut self, board: &mut Board) {
|
||||
self.board_master.abort(board);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
mod master;
|
||||
|
||||
pub use master::MasterInteractor;
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub mod interactors;
|
||||
|
||||
use rand::RngExt;
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use undoredo::{FlushDelta, ResetDelta};
|
||||
|
|
@ -3,10 +3,7 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
board::Board,
|
||||
layout::compounds::ComponentId,
|
||||
rect::Rect2,
|
||||
selections::ComponentSelection,
|
||||
board::Board, layout::compounds::ComponentId, rect::Rect2, selections::ComponentSelection,
|
||||
};
|
||||
|
||||
impl Board {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ use derive_more::Constructor;
|
|||
use undoredo::ResetDelta;
|
||||
|
||||
use crate::{
|
||||
board::Board,
|
||||
interactor::Interactor,
|
||||
layout::LayerId,
|
||||
selections::ComponentSelection,
|
||||
board::Board, interactor::Interactor, layout::LayerId, selections::ComponentSelection,
|
||||
vector::Vector2,
|
||||
};
|
||||
|
||||
|
|
@ -22,8 +19,6 @@ pub struct DragMoveInteractor {
|
|||
}
|
||||
|
||||
impl Interactor for DragMoveInteractor {
|
||||
fn delete(&mut self, _board: &mut Board) {}
|
||||
|
||||
fn hold(&mut self, board: &mut Board, _layer: LayerId, pointer: Vector2<i64>) {
|
||||
board.reset_delta();
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@ impl DragSelectInteractor {
|
|||
}
|
||||
|
||||
impl Interactor for DragSelectInteractor {
|
||||
fn delete(&mut self, _board: &mut Board) {}
|
||||
|
||||
fn hold(&mut self, board: &mut Board, _layer: LayerId, pointer: Vector2<i64>) {
|
||||
self.selection = PersistableSelection::new();
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ impl SelectInteractor {
|
|||
}
|
||||
|
||||
impl Interactor for SelectInteractor {
|
||||
fn delete(&mut self, _board: &mut Board) {}
|
||||
|
||||
fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
|
||||
let contain = if pointer.x >= self.origin.x {
|
||||
SelectionContainMode::Window
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
board::Board,
|
||||
layout::compounds::ComponentId,
|
||||
selections::ComponentSelection,
|
||||
vector::Vector2,
|
||||
board::Board, layout::compounds::ComponentId, selections::ComponentSelection, vector::Vector2,
|
||||
};
|
||||
|
||||
impl Board {
|
||||
|
|
|
|||
|
|
@ -22,20 +22,12 @@ pub trait CompassDirection<T>: Copy + PartialEq + Into<Vector2<T>> {
|
|||
let zero = T::zero();
|
||||
|
||||
if axis.y.is_zero() {
|
||||
let x = if axis.x < zero {
|
||||
-vector.x
|
||||
} else {
|
||||
vector.x
|
||||
};
|
||||
let x = if axis.x < zero { -vector.x } else { vector.x };
|
||||
return Vector2::new(x, zero);
|
||||
}
|
||||
|
||||
if axis.x.is_zero() {
|
||||
let y = if axis.y < zero {
|
||||
-vector.y
|
||||
} else {
|
||||
vector.y
|
||||
};
|
||||
let y = if axis.y < zero { -vector.y } else { vector.y };
|
||||
return Vector2::new(zero, y);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,22 @@
|
|||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
board::{
|
||||
Board,
|
||||
interactors::SelectInteractor,
|
||||
selections::PersistableSelection,
|
||||
},
|
||||
board::{Board, interactors::SelectInteractor, selections::PersistableSelection},
|
||||
layout::LayerId,
|
||||
vector::Vector2,
|
||||
};
|
||||
|
||||
pub trait Interactor {
|
||||
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 step(&mut self, board: &mut Board) {}
|
||||
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) {}
|
||||
}
|
||||
|
||||
pub enum MasterInteractor {
|
||||
Board(crate::board::interactors::MasterInteractor),
|
||||
Autoplacer(crate::autoplacer::interactors::MasterInteractor),
|
||||
}
|
||||
|
||||
impl MasterInteractor {
|
||||
|
|
@ -31,38 +29,51 @@ impl MasterInteractor {
|
|||
pub fn selection(&self) -> &PersistableSelection {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.selection(),
|
||||
Self::Autoplacer(interactor) => interactor.selection(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_interactor(&self) -> &Option<SelectInteractor> {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.select_interactor(),
|
||||
Self::Autoplacer(interactor) => interactor.select_interactor(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactor for MasterInteractor {
|
||||
fn step(&mut self, board: &mut Board) {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.step(board),
|
||||
Self::Autoplacer(interactor) => interactor.step(board),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&mut self, board: &mut Board) {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.delete(board),
|
||||
Self::Autoplacer(interactor) => interactor.delete(board),
|
||||
}
|
||||
}
|
||||
|
||||
fn hold(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.hold(board, layer, pointer),
|
||||
Self::Autoplacer(interactor) => interactor.hold(board, layer, pointer),
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self, board: &mut Board, layer: LayerId, pointer: Vector2<i64>) {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.release(board, layer, pointer),
|
||||
Self::Autoplacer(interactor) => interactor.release(board, layer, pointer),
|
||||
}
|
||||
}
|
||||
|
||||
fn abort(&mut self, board: &mut Board) {
|
||||
match self {
|
||||
Self::Board(interactor) => interactor.abort(board),
|
||||
Self::Autoplacer(interactor) => interactor.abort(board),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,12 +176,10 @@ impl Layout {
|
|||
.iter()
|
||||
.copied()
|
||||
.flat_map(|joint_id| self.locate_joint_infringements(joint_id).map(Into::into))
|
||||
.chain(
|
||||
pin.segments.iter().copied().flat_map(|segment_id| {
|
||||
self.locate_segment_infringements(segment_id)
|
||||
.map(Into::into)
|
||||
}),
|
||||
)
|
||||
.chain(pin.segments.iter().copied().flat_map(|segment_id| {
|
||||
self.locate_segment_infringements(segment_id)
|
||||
.map(Into::into)
|
||||
}))
|
||||
.chain(
|
||||
pin.vias
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -121,11 +121,7 @@ impl Layout {
|
|||
infringer_bbox.intersection(infringee_bbox)
|
||||
}
|
||||
|
||||
pub fn via_via_rect_overlap(
|
||||
&self,
|
||||
infringer: ViaId,
|
||||
infringee: ViaId,
|
||||
) -> Option<Rect2<i64>> {
|
||||
pub fn via_via_rect_overlap(&self, infringer: ViaId, infringee: ViaId) -> Option<Rect2<i64>> {
|
||||
let infringer_bbox = self.via(infringer).bbox().xy();
|
||||
let infringee_bbox = self.via(infringee).bbox().xy();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ use specctra::{
|
|||
|
||||
use crate::{
|
||||
board::{Board, LayerDesc, LayerSide, LayerType},
|
||||
layout::primitives::{JointSpec, Polygon, Segment, SegmentSpec},
|
||||
layout::{
|
||||
LayerId,
|
||||
compounds::{ComponentId, NetId, PinId, PinSpec},
|
||||
},
|
||||
layout::primitives::{JointSpec, Polygon, Segment, SegmentSpec},
|
||||
vector::Vector2,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,16 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoardWorkspace {
|
||||
|
|
|
|||
Loading…
Reference in New Issue