diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index a2354bee5a9e231f0881f58cdee8896439c5dc9a..160b421e9ed35651d097cc330bf5d7af03401b9a 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -11,7 +11,7 @@ use egui::{ Sense, Stroke, Theme, Ui, Vec2, }; use elements::Element; -use grid::GridInfo; +use grid::{GridInfo, LINE_THICKNESS}; use pos::Pos; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; @@ -32,7 +32,7 @@ enum Action { } /// Piping and instrumentation diagram -#[derive(Clone, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Default)] pub struct PidPane { elements: Vec<Element>, connections: Vec<Connection>, @@ -48,22 +48,6 @@ pub struct PidPane { center_content: bool, } -impl Default for PidPane { - fn default() -> Self { - Self { - elements: Vec::default(), - connections: Vec::default(), - grid: GridInfo { - zero_pos: Pos2::ZERO, - size: 10.0, - }, - action: None, - editable: false, - center_content: false, - } - } -} - impl PaneBehavior for PidPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let theme = PidPane::find_theme(ui.ctx()); @@ -83,6 +67,12 @@ impl PaneBehavior for PidPane { let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); let pointer_pos = response.hover_pos(); + if let Some(pointer_pos) = pointer_pos { + if self.editable { + self.handle_zoom(ui, theme, &pointer_pos); + } + } + // Set grab icon when hovering an element if let Some(pointer_pos) = &pointer_pos { if self.editable @@ -98,26 +88,24 @@ impl PaneBehavior for PidPane { if let Some(pointer_pos) = &pointer_pos { if response.clicked_by(PointerButton::Secondary) { println!("Context menu opened"); - self.action = Some(Action::ContextMenu(pointer_pos.clone())); + self.action = Some(Action::ContextMenu(*pointer_pos)); } else if self.editable { if response.drag_started() { if response.dragged_by(PointerButton::Middle) { self.action = Some(Action::DragGrid); println!("Grid drag started"); - } else { - if let Some(drag_element_action) = self - .find_hovered_element_idx(pointer_pos) - .map(|idx| Action::DragElement(idx)) - { - self.action = Some(drag_element_action); - println!("Element drag started"); - } else if let Some(drag_connection_point) = self - .find_hovered_connection_point(pointer_pos) - .map(|(idx1, idx2)| Action::DragConnection(idx1, idx2)) - { - self.action = Some(drag_connection_point); - println!("Connection point drag started"); - } + } else if let Some(drag_element_action) = self + .find_hovered_element_idx(pointer_pos) + .map(Action::DragElement) + { + self.action = Some(drag_element_action); + println!("Element drag started"); + } else if let Some(drag_connection_point) = self + .find_hovered_connection_point(pointer_pos) + .map(|(idx1, idx2)| Action::DragConnection(idx1, idx2)) + { + self.action = Some(drag_connection_point); + println!("Connection point drag started"); } } else if response.drag_stopped() { self.action.take(); @@ -172,20 +160,15 @@ impl PidPane { fn is_hovering_element(&self, pointer_pos: &Pos2) -> bool { self.elements .iter() - .find(|element| element.contains(&self.grid, pointer_pos)) - .is_some() + .any(|element| element.contains(&self.grid, pointer_pos)) } fn is_hovering_connection_point(&self, pointer_pos: &Pos2) -> bool { - self.connections - .iter() - .find(|conn| { - conn.middle_points - .iter() - .find(|p| p.distance(&self.grid, pointer_pos) < 10.0) - .is_some() - }) - .is_some() + self.connections.iter().any(|conn| { + conn.middle_points + .iter() + .any(|p| p.distance(&self.grid, pointer_pos) < 10.0) + }) } /// Returns the currently used theme @@ -227,7 +210,7 @@ impl PidPane { self.connections .iter() .enumerate() - .find_map(|(idx, conn)| Some(idx).zip(conn.contains(&self, pos))) + .find_map(|(idx, conn)| Some(idx).zip(conn.contains(self, pos))) } fn find_hovered_connection_point(&self, pos: &Pos2) -> Option<(usize, usize)> { @@ -248,13 +231,17 @@ impl PidPane { let window_rect = ui.max_rect(); let dot_color = PidPane::dots_color(theme); - let start_x = (window_rect.min.x / self.grid.size) as i32 * self.grid.size as i32; - let end_x = (window_rect.max.x / self.grid.size + 1.0) as i32 * self.grid.size as i32; - let start_y = (window_rect.min.y / self.grid.size) as i32 * self.grid.size as i32; - let end_y = (window_rect.max.y / self.grid.size + 1.0) as i32 * self.grid.size as i32; - - for x in (start_x..end_x).step_by(self.grid.size as usize) { - for y in (start_y..end_y).step_by(self.grid.size as usize) { + let start_x = + (window_rect.min.x / self.grid.get_size()) as i32 * self.grid.get_size() as i32; + let end_x = + (window_rect.max.x / self.grid.get_size() + 1.0) as i32 * self.grid.get_size() as i32; + let start_y = + (window_rect.min.y / self.grid.get_size()) as i32 * self.grid.get_size() as i32; + let end_y = + (window_rect.max.y / self.grid.get_size() + 1.0) as i32 * self.grid.get_size() as i32; + + for x in (start_x..end_x).step_by(self.grid.get_size() as usize) { + for y in (start_y..end_y).step_by(self.grid.get_size() as usize) { let rect = egui::Rect::from_min_size( egui::Pos2::new(x as f32, y as f32), egui::Vec2::new(1.0, 1.0), @@ -284,13 +271,13 @@ impl PidPane { // Draw line segments if points.is_empty() { - PidPane::draw_connection_segment(painter, color, start, end); + self.draw_connection_segment(painter, color, start, end); } else { - PidPane::draw_connection_segment(painter, color, start, *points.first().unwrap()); + self.draw_connection_segment(painter, color, start, *points.first().unwrap()); for i in 0..(points.len() - 1) { - PidPane::draw_connection_segment(painter, color, points[i], points[i + 1]); + self.draw_connection_segment(painter, color, points[i], points[i + 1]); } - PidPane::draw_connection_segment(painter, color, *points.last().unwrap(), end); + self.draw_connection_segment(painter, color, *points.last().unwrap(), end); } // Draw handles (dragging boxes) @@ -299,7 +286,7 @@ impl PidPane { painter.rect( egui::Rect::from_center_size( point, - Vec2::new(self.grid.size, self.grid.size), + Vec2::new(self.grid.get_size(), self.grid.get_size()), ), Rounding::ZERO, Color32::DARK_GRAY, @@ -310,15 +297,18 @@ impl PidPane { } } - fn draw_connection_segment(painter: &Painter, color: Color32, a: Pos2, b: Pos2) { - painter.line_segment([a, b], PathStroke::new(2.0, color)); + fn draw_connection_segment(&self, painter: &Painter, color: Color32, a: Pos2, b: Pos2) { + painter.line_segment( + [a, b], + PathStroke::new(LINE_THICKNESS * self.grid.get_size(), color), + ); } fn draw_elements(&self, ui: &Ui, theme: Theme) { for element in &self.elements { let image_rect = egui::Rect::from_center_size( element.position.to_pos2(&self.grid), - Vec2::splat(element.size as f32 * self.grid.size), + Vec2::splat(element.size as f32 * self.grid.get_size()), ); egui::Image::new(element.symbol.get_image(theme)) @@ -339,8 +329,8 @@ impl PidPane { return; } - if self.is_hovering_element(&pointer_pos) { - let hovered_element = self.find_hovered_element_idx(&pointer_pos); + if self.is_hovering_element(pointer_pos) { + let hovered_element = self.find_hovered_element_idx(pointer_pos); if ui.button("Connect").clicked() { if let Some(idx) = hovered_element { println!("Connect action started"); @@ -351,7 +341,7 @@ impl PidPane { ui.close_menu(); } if ui.button("Rotate 90° ⟲").clicked() { - if let Some(elem) = self.find_hovered_element_mut(&pointer_pos) { + if let Some(elem) = self.find_hovered_element_mut(pointer_pos) { elem.rotation += PI / 2.0; } else { panic!("No element found where the \"Rotate 90° ⟲\" action was issued"); @@ -359,7 +349,7 @@ impl PidPane { ui.close_menu(); } if ui.button("Rotate 90° ⟳").clicked() { - if let Some(elem) = self.find_hovered_element_mut(&pointer_pos) { + if let Some(elem) = self.find_hovered_element_mut(pointer_pos) { elem.rotation -= PI / 2.0; } else { panic!("No element found where the \"Rotate 90° ⟳\" action was issued"); @@ -367,14 +357,14 @@ impl PidPane { ui.close_menu(); } if ui.button("Delete").clicked() { - if let Some(idx) = self.find_hovered_element_idx(&pointer_pos) { + if let Some(idx) = self.find_hovered_element_idx(pointer_pos) { self.delete_element(idx); } else { panic!("No element found where the \"Delete\" action was issued"); } ui.close_menu(); } - } else if let Some((conn_idx, segm_idx)) = self.find_hovered_connection_idx(&pointer_pos) { + } else if let Some((conn_idx, segm_idx)) = self.find_hovered_connection_idx(pointer_pos) { if ui.button("Split").clicked() { println!("Splitting connection line"); self.connections[conn_idx].split(segm_idx, Pos::from_pos2(&self.grid, pointer_pos)); @@ -397,7 +387,7 @@ impl PidPane { for symbol in Symbol::iter() { if ui.button(symbol.to_string()).clicked() { self.elements.push(Element::new( - Pos::from_pos2(&self.grid, &pointer_pos), + Pos::from_pos2(&self.grid, pointer_pos), symbol, )); ui.close_menu(); @@ -445,4 +435,17 @@ impl PidPane { self.grid.zero_pos = ui_center - pid_center.to_vec2(); } + + fn handle_zoom(&mut self, ui: &Ui, theme: Theme, pointer_pos: &Pos2) { + let scroll_delta = ui.input(|i| i.raw_scroll_delta).y; + if scroll_delta != 0.0 { + self.grid.apply_scroll_delta(scroll_delta, pointer_pos); + + // Invalidate the cache to redraw the images + for symbol in Symbol::iter() { + let img: egui::ImageSource = symbol.get_image(theme); + ui.ctx().forget_image(img.uri().unwrap()); + } + } + } } diff --git a/src/ui/panes/pid_drawing_tool/elements.rs b/src/ui/panes/pid_drawing_tool/elements.rs index 4fbc3f8349ec6ee90306ce418a160aa02efd579e..446c444e03cda8d3ab653a1f2b66552912108f5a 100644 --- a/src/ui/panes/pid_drawing_tool/elements.rs +++ b/src/ui/panes/pid_drawing_tool/elements.rs @@ -37,7 +37,7 @@ impl Element { pub fn get_anchor(&self, grid: &GridInfo, idx: usize) -> Pos2 { let anchor = self.symbol.get_anchor_points()[idx]; - let anchor = Vec2::from(anchor) * self.size as f32 * grid.size; + let anchor = Vec2::from(anchor) * self.size as f32 * grid.get_size(); self.position.to_pos2(grid) + anchor } diff --git a/src/ui/panes/pid_drawing_tool/grid.rs b/src/ui/panes/pid_drawing_tool/grid.rs index 1e04422391b3cc98427f08834b76c75064240f1e..9977c69ca1aea025d095866d51e335208c70c371 100644 --- a/src/ui/panes/pid_drawing_tool/grid.rs +++ b/src/ui/panes/pid_drawing_tool/grid.rs @@ -1,10 +1,52 @@ use egui::Pos2; use serde::{Deserialize, Serialize}; -pub const LINE_DISTANCE_THRESHOLD: f32 = 5.0; // Pixels +const DEFAULT_SIZE: f32 = 10.0; +const MIN_SIZE: f32 = 5.0; +const MAX_SIZE: f32 = 50.0; +const SCROLL_DELTA: f32 = 1.0; + +pub const LINE_DISTANCE_THRESHOLD: f32 = 5.0; +pub const LINE_THICKNESS: f32 = 0.2; #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct GridInfo { pub zero_pos: Pos2, - pub size: f32, + size: f32, +} + +impl Default for GridInfo { + fn default() -> Self { + Self { + zero_pos: Pos2::ZERO, + size: DEFAULT_SIZE, + } + } +} + +impl GridInfo { + pub fn get_size(&self) -> f32 { + self.size + } + + pub fn apply_scroll_delta(&mut self, delta: f32, pos: &Pos2) { + if delta == 0.0 { + return; + } + + let old_size = self.size; + if delta > 0.0 { + self.size += SCROLL_DELTA; + } else { + self.size -= SCROLL_DELTA; + }; + + self.size = self.size.clamp(MIN_SIZE, MAX_SIZE); + + if self.size != old_size { + let delta_prop = self.size / old_size - 1.0; + let pos_delta = delta_prop * (self.zero_pos - pos.to_vec2()); + self.zero_pos += pos_delta.to_vec2(); + } + } } diff --git a/src/ui/panes/pid_drawing_tool/pos.rs b/src/ui/panes/pid_drawing_tool/pos.rs index 88178616c6a12ac969c3c89fbeb8642009026d33..bb91852492a61f3f3885b98cb2b846cfae0f641f 100644 --- a/src/ui/panes/pid_drawing_tool/pos.rs +++ b/src/ui/panes/pid_drawing_tool/pos.rs @@ -23,22 +23,22 @@ impl Pos { pub fn to_pos2(&self, grid: &GridInfo) -> Pos2 { Pos2 { - x: self.x as f32 * grid.size + grid.zero_pos.x, - y: self.y as f32 * grid.size + grid.zero_pos.y, + x: self.x as f32 * grid.get_size() + grid.zero_pos.x, + y: self.y as f32 * grid.get_size() + grid.zero_pos.y, } } pub fn to_relative_pos2(&self, grid: &GridInfo) -> Pos2 { Pos2 { - x: self.x as f32 * grid.size, - y: self.y as f32 * grid.size, + x: self.x as f32 * grid.get_size(), + y: self.y as f32 * grid.get_size(), } } pub fn from_pos2(grid: &GridInfo, pos: &Pos2) -> Self { Self { - x: ((pos.x - grid.zero_pos.x) / grid.size) as i32, - y: ((pos.y - grid.zero_pos.y) / grid.size) as i32, + x: ((pos.x - grid.zero_pos.x) / grid.get_size()) as i32, + y: ((pos.y - grid.zero_pos.y) / grid.get_size()) as i32, } }