diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 34b989c795017af302c9eb76b8ef7af87fd680cc..1dcf51d99fcb73a3648e78af0eb35fcb6a7a67f8 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -1,13 +1,16 @@ mod connections; mod elements; +mod grid; mod pos; mod symbols; use connections::Connection; use egui::{ - epaint::PathStroke, Color32, Context, CursorIcon, PointerButton, Pos2, Sense, Theme, Ui, Vec2, + epaint::PathStroke, Color32, Context, CursorIcon, PointerButton, Pos2, Rounding, Sense, Stroke, + Theme, Ui, Vec2, }; use elements::Element; +use grid::GridInfo; use pos::Pos; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; @@ -21,8 +24,9 @@ use super::PaneBehavior; #[derive(Clone, Serialize, Deserialize, PartialEq)] enum Action { Connect(usize), - ContextMenu(Pos), - Drag(usize), + ContextMenu(Pos2), + DragElement(usize), + DragConnection(usize, usize), } /// Piping and instrumentation diagram @@ -31,7 +35,7 @@ pub struct PidPane { elements: Vec<Element>, connections: Vec<Connection>, - grid_size: f32, + grid: GridInfo, #[serde(skip)] action: Option<Action>, @@ -42,7 +46,7 @@ impl Default for PidPane { Self { elements: Vec::default(), connections: Vec::default(), - grid_size: 10.0, + grid: GridInfo { size: 10.0 }, action: None, } @@ -58,11 +62,13 @@ impl PaneBehavior for PidPane { // Allocate the space to sense inputs let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); - let pointer_pos = response.hover_pos().map(|pos| self.screen_to_grid_pos(pos)); + let pointer_pos = response.hover_pos(); // Set grab icon when hovering an element if let Some(pointer_pos) = &pointer_pos { - if self.is_hovering_element(pointer_pos) { + if self.is_hovering_element(pointer_pos) + || self.is_hovering_connection_point(pointer_pos) + { ui.ctx() .output_mut(|output| output.cursor_icon = CursorIcon::Grab); } @@ -74,10 +80,21 @@ impl PaneBehavior for PidPane { println!("Context menu opened"); self.action = Some(Action::ContextMenu(pointer_pos.clone())); } else if response.drag_started() { - println!("Drag started"); - self.action = self + println!("Checking drag start at {:?}", pointer_pos); + 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"); + } + if let Some(drag_element_action) = self .find_hovered_element_idx(pointer_pos) - .map(|idx| Action::Drag(idx)); + .map(|idx| Action::DragElement(idx)) + { + self.action = Some(drag_element_action); + println!("Element drag started"); + } } else if response.drag_stopped() { self.action.take(); println!("Drag stopped"); @@ -96,7 +113,7 @@ impl PaneBehavior for PidPane { if let Some(end) = self.find_hovered_element_idx(&pointer_pos) { if response.clicked() { if start != end { - self.connections.push(Connection { start, end }); + self.connections.push(Connection::new(start, end)); println!("Added connection from {} to {}", start, end); } self.action.take(); @@ -104,10 +121,12 @@ impl PaneBehavior for PidPane { } } } - Some(Action::Drag(idx)) => { - let element = &mut self.elements[idx]; - element.position.x = pointer_pos.x - element.size / 2; - element.position.y = pointer_pos.y - element.size / 2; + Some(Action::DragElement(idx)) => { + self.elements[idx].position = Pos::from_pos2(&self.grid, &pointer_pos) + } + Some(Action::DragConnection(conn_idx, midpoint_idx)) => { + self.connections[conn_idx].middle_points[midpoint_idx] = + Pos::from_pos2(&self.grid, &pointer_pos); } _ => {} } @@ -122,10 +141,22 @@ impl PaneBehavior for PidPane { } impl PidPane { - fn is_hovering_element(&self, pointer_pos: &Pos) -> bool { + fn is_hovering_element(&self, pointer_pos: &Pos2) -> bool { self.elements .iter() - .find(|element| element.contains(pointer_pos)) + .find(|element| element.contains(&self.grid, pointer_pos)) + .is_some() + } + + 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() } @@ -151,21 +182,37 @@ impl PidPane { } } - fn screen_to_grid_pos(&self, screen_pos: Pos2) -> Pos { - Pos { - x: (screen_pos.x / self.grid_size) as i32, - y: (screen_pos.y / self.grid_size) as i32, - } - } - - fn find_hovered_element_idx(&self, pos: &Pos) -> Option<usize> { - self.elements.iter().position(|elem| elem.contains(&pos)) + fn find_hovered_element_idx(&self, pos: &Pos2) -> Option<usize> { + self.elements + .iter() + .position(|elem| elem.contains(&self.grid, pos)) } - fn find_hovered_element_mut(&mut self, pos: &Pos) -> Option<&mut Element> { + fn find_hovered_element_mut(&mut self, pos: &Pos2) -> Option<&mut Element> { self.elements .iter_mut() - .find(|element| element.contains(&pos)) + .find(|element| element.contains(&self.grid, pos)) + } + + /// Return the connection and segment indexes where the position is on, if any + fn find_hovered_connection_idx(&self, pos: &Pos2) -> Option<(usize, usize)> { + self.connections + .iter() + .enumerate() + .find_map(|(idx, conn)| Some(idx).zip(conn.contains(&self, pos))) + } + + fn find_hovered_connection_point(&self, pos: &Pos2) -> Option<(usize, usize)> { + let mut midpoint_idx = Some(0); + let connection_idx = self.connections.iter().position(|conn| { + midpoint_idx = conn + .middle_points + .iter() + .position(|p| p.distance(&self.grid, pos) < 12.0); + midpoint_idx.is_some() + }); + + connection_idx.zip(midpoint_idx) } fn draw_grid(&self, theme: Theme, ui: &Ui) { @@ -174,10 +221,10 @@ impl PidPane { let dot_color = PidPane::dots_color(theme); for x in (window_rect.min.x as i32..window_rect.max.x.round() as i32) - .step_by(self.grid_size as usize) + .step_by(self.grid.size as usize) { for y in (window_rect.min.y as i32..window_rect.max.y.round() as i32) - .step_by(self.grid_size as usize) + .step_by(self.grid.size as usize) { let rect = egui::Rect::from_min_size( egui::Pos2::new(x as f32, y as f32), @@ -192,31 +239,55 @@ impl PidPane { let painter = ui.painter(); for connection in &self.connections { - let elem1 = &self.elements[connection.start]; - let elem2 = &self.elements[connection.end]; - - let x1 = (elem1.position.x + elem1.size / 2) as f32 * self.grid_size; - let y1 = (elem1.position.y + elem1.size / 2) as f32 * self.grid_size; - let x2 = (elem2.position.x + elem2.size / 2) as f32 * self.grid_size; - let y2 = (elem2.position.y + elem2.size / 2) as f32 * self.grid_size; + let mut points = Vec::new(); + + // Append start point + let start = &self.elements[connection.start]; + points.push(start.position.into_pos2(&self.grid)); + + // Append all midpoints + connection + .middle_points + .iter() + .map(|p| p.into_pos2(&self.grid)) + .for_each(|p| points.push(p)); + + // Append end point + let end = &self.elements[connection.end]; + points.push(end.position.into_pos2(&self.grid)); + + // Draw line segments + for i in 0..(points.len() - 1) { + let a = points[i]; + let b = points[i + 1]; + painter.line_segment([a, b], PathStroke::new(1.0, Color32::GREEN)); + } - painter.line_segment( - [Pos2::new(x1, y1), Pos2::new(x2, y2)], - PathStroke::new(1.0, Color32::GREEN), - ); + // Draw dragging boxes + for middle_point in &connection.middle_points { + painter.rect( + egui::Rect::from_center_size( + middle_point.into_pos2(&self.grid), + Vec2::new(self.grid.size, self.grid.size), + ), + Rounding::ZERO, + Color32::DARK_GRAY, + Stroke::NONE, + ); + } } } fn draw_elements(&self, theme: Theme, ui: &Ui) { for element in &self.elements { - let image_rect = egui::Rect::from_min_size( + let image_rect = egui::Rect::from_center_size( egui::Pos2::new( - element.position.x as f32 * self.grid_size, - element.position.y as f32 * self.grid_size, + element.position.x as f32 * self.grid.size, + element.position.y as f32 * self.grid.size, ), egui::Vec2::new( - element.size as f32 * self.grid_size, - element.size as f32 * self.grid_size, + element.size as f32 * self.grid.size, + element.size as f32 * self.grid.size, ), ); @@ -226,7 +297,7 @@ impl PidPane { } } - fn draw_context_menu(&mut self, pointer_pos: &Pos, ui: &mut Ui) { + fn draw_context_menu(&mut self, pointer_pos: &Pos2, ui: &mut Ui) { ui.set_max_width(100.0); // To make sure we wrap long text if self.is_hovering_element(&pointer_pos) { @@ -264,11 +335,20 @@ impl PidPane { } ui.close_menu(); } + } 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)); + ui.close_menu(); + } } else { ui.menu_button("Symbols", |ui| { for symbol in Symbol::iter() { if ui.button(symbol.to_string()).clicked() { - self.elements.push(Element::new(pointer_pos, symbol)); + self.elements.push(Element::new( + Pos::from_pos2(&self.grid, &pointer_pos), + symbol, + )); ui.close_menu(); } } diff --git a/src/ui/panes/pid_drawing_tool/connections.rs b/src/ui/panes/pid_drawing_tool/connections.rs new file mode 100644 index 0000000000000000000000000000000000000000..e1c9a3b9f3fa1e72c3e74e5003f9de828b794113 --- /dev/null +++ b/src/ui/panes/pid_drawing_tool/connections.rs @@ -0,0 +1,102 @@ +use egui::Pos2; +use serde::{Deserialize, Serialize}; + +use super::{grid::LINE_DISTANCE_THRESHOLD, pos::Pos, PidPane}; + +#[derive(Clone, Serialize, Deserialize, PartialEq)] +pub struct Connection { + /// Index of the start element + pub start: usize, + + /// Index of the end element + pub end: usize, + + /// Coordinates of middle points + pub middle_points: Vec<Pos>, +} + +impl Connection { + pub fn new(start: usize, end: usize) -> Self { + Self { + start, + end, + middle_points: Vec::new(), + } + } + + /// Return the index of the segment the position is on, if any + pub fn contains(&self, pid: &PidPane, pos: &Pos2) -> Option<usize> { + let mut points = Vec::new(); + + // Append start point + let start = &pid.elements[self.start]; + points.push(start.position.into_pos2(&pid.grid)); + + // Append all midpoints + self.middle_points + .iter() + .map(|p| p.into_pos2(&pid.grid)) + .for_each(|p| points.push(p)); + + // Append end point + let end = &pid.elements[self.end]; + points.push(end.position.into_pos2(&pid.grid)); + + // Check each segment + for i in 0..(points.len() - 1) { + let a = points[i]; + let b = points[i + 1]; + if is_hovering_segment(pos, &a, &b) { + return Some(i); + } + } + + None + } + + pub fn split(&mut self, idx: usize, pos: Pos) { + self.middle_points.insert(idx, pos.clone()); + } +} + +fn distance(a: &Pos2, b: &Pos2) -> f32 { + ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt() +} + +/// Distance of a from the line defined by b and c +fn distance_from_line(p: &Pos2, m: f32, q: f32) -> f32 { + (p.y - m * p.x - q).abs() / (1.0 + m * m).sqrt() +} + +/// True if p hovers the segment defined by a and b +fn is_hovering_segment(p: &Pos2, a: &Pos2, b: &Pos2) -> bool { + if a != b { + let midpoint = Pos2 { + x: (a.x + b.x) / 2.0, + y: (a.y + b.y) / 2.0, + }; + let m = (a.y - b.y) / (a.x - b.x); + + let (d1, d2) = if m == 0.0 { + ((p.y - midpoint.y).abs(), (p.x - midpoint.x).abs()) + } else if m == f32::INFINITY { + ((p.x - midpoint.x).abs(), (p.y - midpoint.y).abs()) + } else { + let q = (a.x * b.y - b.x * a.y) / (a.x - b.x); + + let m_inv = -1.0 / m; + let q_inv = midpoint.y - m_inv * midpoint.x; + + ( + distance_from_line(p, m, q), + distance_from_line(p, m_inv, q_inv), + ) + }; + + let length = distance(a, b); + + d1 <= LINE_DISTANCE_THRESHOLD && d2 <= length + } else { + false + } +} diff --git a/src/ui/panes/pid_drawing_tool/elements.rs b/src/ui/panes/pid_drawing_tool/elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..97f11363b7d26d088d442b05c5d663a71f433f7b --- /dev/null +++ b/src/ui/panes/pid_drawing_tool/elements.rs @@ -0,0 +1,37 @@ +use super::symbols::Symbol; +use super::{grid::GridInfo, pos::Pos}; +use egui::Pos2; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize, PartialEq)] +pub struct Element { + /// Ancor postion in the grid, symbol center + pub position: Pos, + + /// Size in grid units + pub size: i32, + + /// Rotation in radiants + pub rotation: f32, + + /// Symbol to be displayed + pub symbol: Symbol, +} + +impl Element { + pub fn new(pos: Pos, symbol: Symbol) -> Self { + Self { + position: pos, + size: 10, + rotation: 0.0, + symbol, + } + } + + pub fn contains(&self, grid: &GridInfo, pos: &Pos2) -> bool { + let start = self.position.add_size(-self.size / 2).into_pos2(grid); + let end = self.position.add_size(self.size / 2).into_pos2(grid); + + (start.x <= pos.x && pos.x < end.x) && (start.y <= pos.y && pos.y < end.y) + } +} diff --git a/src/ui/panes/pid_drawing_tool/grid.rs b/src/ui/panes/pid_drawing_tool/grid.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8adc1bb7b995c37403f58f07efe5df829d767bb --- /dev/null +++ b/src/ui/panes/pid_drawing_tool/grid.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +pub const LINE_DISTANCE_THRESHOLD: f32 = 5.0; // Pixels + +#[derive(Clone, Serialize, Deserialize, PartialEq)] +pub struct GridInfo { + pub size: f32, +} diff --git a/src/ui/panes/pid_drawing_tool/pos.rs b/src/ui/panes/pid_drawing_tool/pos.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a8b0a8873c1db1f0b1e39ef9f1a0c7500be8127 --- /dev/null +++ b/src/ui/panes/pid_drawing_tool/pos.rs @@ -0,0 +1,39 @@ +use egui::Pos2; +use serde::{Deserialize, Serialize}; + +use super::grid::GridInfo; + +#[derive(Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct Pos { + pub x: i32, + pub y: i32, +} + +impl Pos { + pub fn add_size(&self, size: i32) -> Self { + Self { + x: self.x + size, + y: self.y + size, + } + } + + pub fn into_pos2(&self, grid: &GridInfo) -> Pos2 { + Pos2 { + x: self.x as f32 * grid.size, + y: self.y as f32 * grid.size, + } + } + + pub fn from_pos2(grid: &GridInfo, pos: &Pos2) -> Self { + Self { + x: (pos.x / grid.size) as i32, + y: (pos.y / grid.size) as i32, + } + } + + pub fn distance(&self, grid: &GridInfo, pos: &Pos2) -> f32 { + let me = self.into_pos2(grid); + + ((me.x - pos.x).powi(2) + (me.y - pos.y).powi(2)).sqrt() + } +}