From 347bfcfbdc32f47f79e8ce872a66eb9db5f36269 Mon Sep 17 00:00:00 2001
From: Alberto Nidasio <alberto.nidasio@skywarder.eu>
Date: Mon, 13 Jan 2025 11:08:22 +0100
Subject: [PATCH] Implemented splitting PID connections into multiple segments
---
src/ui/panes/pid_drawing_tool.rs | 176 ++++++++++++++-----
src/ui/panes/pid_drawing_tool/connections.rs | 102 +++++++++++
src/ui/panes/pid_drawing_tool/elements.rs | 37 ++++
src/ui/panes/pid_drawing_tool/grid.rs | 8 +
src/ui/panes/pid_drawing_tool/pos.rs | 39 ++++
5 files changed, 314 insertions(+), 48 deletions(-)
create mode 100644 src/ui/panes/pid_drawing_tool/connections.rs
create mode 100644 src/ui/panes/pid_drawing_tool/elements.rs
create mode 100644 src/ui/panes/pid_drawing_tool/grid.rs
create mode 100644 src/ui/panes/pid_drawing_tool/pos.rs
diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs
index 34b989c..1dcf51d 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 0000000..e1c9a3b
--- /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 0000000..97f1136
--- /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 0000000..e8adc1b
--- /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 0000000..4a8b0a8
--- /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()
+ }
+}
--
GitLab