From c8848ef269f7157d1a627203e2e7a56a28d2cb4c Mon Sep 17 00:00:00 2001 From: Ellen Emilia Anna Zscheile Date: Fri, 27 Jun 2025 06:50:01 +0200 Subject: [PATCH] refactor(topola-egui): Move InteractiveEvent handling from Viewport into Workspace --- crates/topola-egui/src/overlay.rs | 7 +- crates/topola-egui/src/viewport.rs | 147 ++++++++++------------------ crates/topola-egui/src/workspace.rs | 98 ++++++++++++++++--- src/interactor/activity.rs | 16 ++- src/interactor/route_plan.rs | 8 +- 5 files changed, 159 insertions(+), 117 deletions(-) diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index 80ea35d..f3af86d 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -131,13 +131,14 @@ impl Overlay { _board: &Board, _appearance_panel: &AppearancePanel, at: Point, - modifiers: &egui::Modifiers, + ctrl: bool, + shift: bool, ) { if self.reselect_bbox.is_none() { // handle bounding box selection - let selmode = if modifiers.ctrl { + let selmode = if ctrl { SelectionMode::Toggling - } else if modifiers.shift { + } else if shift { SelectionMode::Addition } else { SelectionMode::Substitution diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index b23021a..77181dd 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: MIT -use core::ops::ControlFlow; use geo::point; use petgraph::{ data::DataMap, @@ -10,12 +9,8 @@ use petgraph::{ }; use rstar::{Envelope, AABB}; use topola::{ - autorouter::{ - execution::Command, - invoker::{ - GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, - GetObstacles, - }, + autorouter::invoker::{ + GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles, }, board::AccessMesadata, drawing::{ @@ -25,10 +20,10 @@ use topola::{ geometry::{shape::AccessShape, GenericNode}, graph::MakeRef, interactor::{ - activity::{ActivityStepper, InteractiveEvent, InteractiveInput}, + activity::{ActivityStepper, InteractiveEvent, InteractiveEventKind, InteractiveInput}, interaction::InteractionStepper, }, - layout::{poly::MakePolygon, via::ViaWeight}, + layout::poly::MakePolygon, math::{Circle, RotationSense}, router::navmesh::NavnodeIndex, }; @@ -138,95 +133,52 @@ impl Viewport { if let Some(workspace) = maybe_workspace { let latest_point = point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}; - // Advances the app's state by the delta time `dt`. May call - // `.update_state()` more than once if the delta time is more than a multiple of - // the timestep. - let dt = ctx.input(|i| i.stable_dt); - let active_layer = workspace.appearance_panel.active_layer; - self.update_counter += dt; - while self.update_counter >= menu_bar.frame_timestep { - self.update_counter -= menu_bar.frame_timestep; - if let ControlFlow::Break(()) = workspace.update_state( - tr, - error_dialog, - &InteractiveInput { - active_layer, - pointer_pos: point! {x: latest_pos.x as f64, y: latest_pos.y as f64}, - dt, - }, - ) { - break; - } - } + let interactive_input = InteractiveInput { + active_layer: workspace.appearance_panel.active_layer, + pointer_pos: latest_point, + dt: ctx.input(|i| i.stable_dt), + }; + workspace.advance_state_by_dt( + tr, + error_dialog, + menu_bar.frame_timestep, + &interactive_input, + ); - if !workspace.interactor.maybe_activity().as_ref().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ControlFlow::Break(..))) - }) { - // there is currently some activity - let interactive_event = if response.clicked_by(egui::PointerButton::Primary) { - Some(InteractiveEvent::PointerPrimaryButtonClicked) + let interactive_event_kind = + if response.clicked_by(egui::PointerButton::Primary) { + Some(InteractiveEventKind::PointerPrimaryButtonClicked) + } else if response.drag_started_by(egui::PointerButton::Primary) { + Some(InteractiveEventKind::PointerPrimaryButtonDragStarted) + } else if response.drag_stopped_by(egui::PointerButton::Primary) { + Some(InteractiveEventKind::PointerPrimaryButtonDragStopped) } else if response.clicked_by(egui::PointerButton::Secondary) { - Some(InteractiveEvent::PointerSecondaryButtonClicked) + Some(InteractiveEventKind::PointerSecondaryButtonClicked) } else { None }; - if let Some(interactive_event) = interactive_event { - let dt = ctx.input(|i| i.stable_dt); - let active_layer = workspace.appearance_panel.active_layer; - let _ = workspace.update_state_for_event( - tr, - error_dialog, - &InteractiveInput { - active_layer, - pointer_pos: latest_point, - dt, - }, - interactive_event, - ); - } - } else { - let layers = &mut workspace.appearance_panel; - let overlay = &mut workspace.overlay; - let board = workspace.interactor.invoker().autorouter().board(); - if response.clicked_by(egui::PointerButton::Primary) { - if menu_bar.is_placing_via { - workspace.interactor.execute(Command::PlaceVia(ViaWeight { - from_layer: 0, - to_layer: 0, - circle: Circle { - pos: latest_point, - r: menu_bar - .autorouter_options - .router_options - .routed_band_width - / 2.0, - }, - maybe_net: Some(1234), - })); - } else { - overlay.click(board, layers, latest_point); - } - } else if response.drag_started_by(egui::PointerButton::Primary) { - overlay.drag_start( - board, - layers, - latest_point, - &response.ctx.input(|i| i.modifiers), - ); - } else if response.drag_stopped_by(egui::PointerButton::Primary) { - overlay.drag_stop(board, layers, latest_point); - } else if let Some((_, bsk, cur_bbox)) = - overlay.get_bbox_reselect(latest_point) - { - use topola::autorouter::selection::BboxSelectionKind; - painter.paint_bbox_with_color( - cur_bbox, - match bsk { - BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW, - BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE, - }, - ); - } + if let Some(kind) = interactive_event_kind { + let (ctrl, shift) = response + .ctx + .input(|i| (i.modifiers.ctrl, i.modifiers.shift)); + let _ = workspace.update_state_for_event( + tr, + error_dialog, + menu_bar, + &interactive_input, + InteractiveEvent { kind, ctrl, shift }, + ); + } else if let Some((_, bsk, cur_bbox)) = + workspace.overlay.get_bbox_reselect(latest_point) + { + use topola::autorouter::selection::BboxSelectionKind; + painter.paint_bbox_with_color( + cur_bbox, + match bsk { + BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW, + BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE, + }, + ); } let layers = &mut workspace.appearance_panel; @@ -529,8 +481,13 @@ impl Viewport { .paint_primitive(ghost, egui::Color32::from_rgb(75, 75, 150)); } - if let ActivityStepper::Interaction(InteractionStepper::RoutePlan(rp)) = activity.activity() { - painter.paint_linestring(&rp.lines, egui::Color32::from_rgb(245, 182, 66)); + if let ActivityStepper::Interaction(InteractionStepper::RoutePlan(rp)) = + activity.activity() + { + painter.paint_linestring( + &rp.lines, + egui::Color32::from_rgb(245, 182, 66), + ); } if let Some(ref navmesh) = diff --git a/crates/topola-egui/src/workspace.rs b/crates/topola-egui/src/workspace.rs index 087fe6d..44dc06f 100644 --- a/crates/topola-egui/src/workspace.rs +++ b/crates/topola-egui/src/workspace.rs @@ -8,18 +8,19 @@ use std::{ }; use topola::{ - autorouter::history::History, + autorouter::{execution::Command, history::History}, interactor::{ - activity::{InteractiveEvent, InteractiveInput}, + activity::{InteractiveEvent, InteractiveEventKind, InteractiveInput}, Interactor, }, - layout::LayoutEdit, + layout::{via::ViaWeight, LayoutEdit}, + math::Circle, specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata}, }; use crate::{ - appearance_panel::AppearancePanel, error_dialog::ErrorDialog, overlay::Overlay, - translator::Translator, + appearance_panel::AppearancePanel, error_dialog::ErrorDialog, menu_bar::MenuBar, + overlay::Overlay, translator::Translator, }; /// A loaded design and associated structures. @@ -33,6 +34,8 @@ pub struct Workspace { Sender>>, Receiver>>, ), + + update_counter: f32, } impl Workspace { @@ -58,25 +61,94 @@ impl Workspace { ) })?, history_channel: channel(), + update_counter: 0.0, }) } + /// Advances the app's state by the delta time `dt`. May call + /// `.update_state()` more than once if the delta time is more than a multiple of + /// the timestep. + pub fn advance_state_by_dt( + &mut self, + tr: &Translator, + error_dialog: &mut ErrorDialog, + frame_timestep: f32, + interactive_input: &InteractiveInput, + ) { + self.update_counter += interactive_input.dt; + while self.update_counter >= frame_timestep { + self.update_counter -= frame_timestep; + if let ControlFlow::Break(()) = self.update_state(tr, error_dialog, interactive_input) { + break; + } + } + } + pub fn update_state_for_event( &mut self, tr: &Translator, error_dialog: &mut ErrorDialog, + menu_bar: &MenuBar, interactive_input: &InteractiveInput, interactive_event: InteractiveEvent, - ) -> ControlFlow<()> { - match self + ) { + if !self .interactor - .update_for_event(interactive_input, interactive_event) + .maybe_activity() + .as_ref() + .map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ControlFlow::Break(..))) + }) { - ControlFlow::Continue(()) => ControlFlow::Continue(()), - ControlFlow::Break(Ok(())) => ControlFlow::Break(()), - ControlFlow::Break(Err(err)) => { - error_dialog.push_error("tr-module-invoker", format!("{}", err)); - ControlFlow::Break(()) + match self + .interactor + .update_for_event(interactive_input, interactive_event) + { + ControlFlow::Continue(()) | ControlFlow::Break(Ok(())) => {} + ControlFlow::Break(Err(err)) => { + error_dialog.push_error("tr-module-invoker", format!("{}", err)); + } + } + } else { + let board = self.interactor.invoker().autorouter().board(); + match interactive_event.kind { + InteractiveEventKind::PointerPrimaryButtonClicked => { + if menu_bar.is_placing_via { + self.interactor.execute(Command::PlaceVia(ViaWeight { + from_layer: 0, + to_layer: 0, + circle: Circle { + pos: interactive_input.pointer_pos, + r: menu_bar.autorouter_options.router_options.routed_band_width + / 2.0, + }, + maybe_net: Some(1234), + })); + } else { + self.overlay.click( + board, + &self.appearance_panel, + interactive_input.pointer_pos, + ); + } + } + InteractiveEventKind::PointerPrimaryButtonDragStarted => { + self.overlay.drag_start( + board, + &self.appearance_panel, + interactive_input.pointer_pos, + interactive_event.ctrl, + interactive_event.shift, + ); + } + InteractiveEventKind::PointerPrimaryButtonDragStopped => { + self.overlay.drag_stop( + board, + &self.appearance_panel, + interactive_input.pointer_pos, + ); + } + _ => {} } } } diff --git a/src/interactor/activity.rs b/src/interactor/activity.rs index 26a7670..9b0aeaa 100644 --- a/src/interactor/activity.rs +++ b/src/interactor/activity.rs @@ -35,13 +35,25 @@ pub struct InteractiveInput { pub dt: f32, } -/// An event received from the user #[derive(Clone, Copy, Debug)] -pub enum InteractiveEvent { +pub enum InteractiveEventKind { PointerPrimaryButtonClicked, + PointerPrimaryButtonDragStarted, + PointerPrimaryButtonDragStopped, PointerSecondaryButtonClicked, } +/// An event received from the user +pub struct InteractiveEvent { + pub kind: InteractiveEventKind, + + /// `true` if the `Ctrl` key pressed during the event + pub ctrl: bool, + + /// `true` if the `Shift` key pressed during the event + pub shift: bool, +} + /// This is the execution context passed to the stepper on each step pub struct ActivityContext<'a, M> { pub interactive_input: &'a InteractiveInput, diff --git a/src/interactor/route_plan.rs b/src/interactor/route_plan.rs index b82cb45..e0cb3c0 100644 --- a/src/interactor/route_plan.rs +++ b/src/interactor/route_plan.rs @@ -15,7 +15,7 @@ use crate::{ }; use super::{ - activity::{ActivityContext, InteractiveEvent}, + activity::{ActivityContext, InteractiveEvent, InteractiveEventKind}, interaction::InteractionError, }; @@ -128,8 +128,8 @@ impl OnEvent, InteractiveEvent> for Ro context: &mut ActivityContext, event: InteractiveEvent, ) -> Result<(), InteractionError> { - match event { - InteractiveEvent::PointerPrimaryButtonClicked if self.end_pin.is_none() => { + match event.kind { + InteractiveEventKind::PointerPrimaryButtonClicked if self.end_pin.is_none() => { if let Some((layer, idx, pos)) = try_select_pin(context) { // make sure double-click or such doesn't corrupt state if let Some(start_pin) = self.start_pin { @@ -151,7 +151,7 @@ impl OnEvent, InteractiveEvent> for Ro } } } - InteractiveEvent::PointerSecondaryButtonClicked => { + InteractiveEventKind::PointerSecondaryButtonClicked => { if self.end_pin.is_some() { log::debug!("un-clicked end pin"); self.end_pin = None;