diff --git a/Cargo.lock b/Cargo.lock index 1cc3a5f2..8c443d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,7 +865,7 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.15.0" +version = "0.16.0-beta.0" dependencies = [ "crossbeam", "egui", diff --git a/Cargo.toml b/Cargo.toml index 6528fc00..b06f90d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_graphs" -version = "0.15.0" +version = "0.16.0-beta.0" authors = ["Dmitrii Samsonov "] license = "MIT" homepage = "https://github.com/blitzarx1/egui_graphs" @@ -11,7 +11,10 @@ edition = "2021" [dependencies] egui = { version = "0.23", default-features = false } rand = "0.8" -petgraph = { version = "0.6", default-features = false, features = ["stable_graph", "matrix_graph"] } +petgraph = { version = "0.6", default-features = false, features = [ + "stable_graph", + "matrix_graph", +] } crossbeam = { version = "0.8", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } diff --git a/README.md b/README.md index a7b38ef2..672d8c28 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The project implements a Widget for the egui framework, enabling easy visualizat - [x] Style configuration via egui context styles; - [x] Dark/Light theme support via egui context styles; - [x] Events reporting to extend the graph functionality by the user handling them; -- [ ] Edge labels (for the moment there is a `custom_draw` example which demonstrates labels drawing for edges); +- [ ] Edge labels; ## Status The project is on track for a stable release v1.0.0. For the moment, breaking releases are still possible. @@ -80,7 +80,14 @@ Now, lets implement the `update()` function for the `BasicApp`. This function cr impl App for BasicApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut GraphView::< + _, + _, + _, + _, + DefaultNodeShape, + DefaultEdgeShape<_>, + >new(&mut self.g)); }); } } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index d4edd7fb..b941cfe1 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,13 +1,10 @@ use eframe::{run_native, App, CreationContext}; use egui::Context; -use egui_graphs::{Graph, GraphView}; -use petgraph::{ - stable_graph::{DefaultIx, StableGraph}, - Directed, -}; +use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph, GraphView}; +use petgraph::stable_graph::StableGraph; pub struct BasicApp { - g: Graph<(), (), Directed, DefaultIx>, + g: Graph<(), ()>, } impl BasicApp { @@ -20,7 +17,14 @@ impl BasicApp { impl App for BasicApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut GraphView::< + _, + _, + _, + _, + DefaultNodeShape, + DefaultEdgeShape<_>, + >::new(&mut self.g)); }); } } diff --git a/examples/configurable/src/main.rs b/examples/configurable/src/main.rs index 54a3dfdd..cf211590 100644 --- a/examples/configurable/src/main.rs +++ b/examples/configurable/src/main.rs @@ -2,9 +2,9 @@ use std::time::Instant; use crossbeam::channel::{unbounded, Receiver, Sender}; use eframe::{run_native, App, CreationContext}; -use egui::{CollapsingHeader, Context, ScrollArea, Slider, Ui, Vec2}; +use egui::{CollapsingHeader, Context, Pos2, ScrollArea, Slider, Ui}; use egui_graphs::events::Event; -use egui_graphs::{to_graph, Edge, Graph, GraphView, Node}; +use egui_graphs::{to_graph, DefaultEdgeShape, DefaultNodeShape, Edge, Graph, GraphView, Node}; use fdg_sim::glam::Vec3; use fdg_sim::{ForceGraph, ForceGraphHelper, Simulation, SimulationParameters}; use petgraph::stable_graph::{DefaultIx, EdgeIndex, NodeIndex, StableGraph}; @@ -27,8 +27,8 @@ pub struct ConfigurableApp { settings_navigation: SettingsNavigation, settings_style: SettingsStyle, - selected_nodes: Vec>, - selected_edges: Vec>, + selected_nodes: Vec>, + selected_edges: Vec>, last_events: Vec, simulation_stopped: bool, @@ -135,7 +135,7 @@ impl ConfigurableApp { let sim_n = self.sim.get_graph_mut().node_weight_mut(*g_n_idx).unwrap(); let loc = sim_n.location; - g_n.set_location(Vec2::new(loc.x, loc.y)); + g_n.set_location(Pos2::new(loc.x, loc.y)); if g_n.selected() { self.selected_nodes.push(g_n.clone()); @@ -244,12 +244,14 @@ impl ConfigurableApp { // location of new node is in surrounging of random existing node let mut rng = rand::thread_rng(); - let location = Vec2::new( + let location = Pos2::new( random_n.location().x + 10. + rng.gen_range(0. ..50.), random_n.location().y + 10. + rng.gen_range(0. ..50.), ); - let idx = self.g.g.add_node(Node::new(location, ())); + let idx = self.g.g.add_node(Node::new(())); + self.g.g[idx].bind(idx, location); + let n = self.g.g.node_weight_mut(idx).unwrap(); *n = n.with_label(format!("{:?}", idx)); let mut sim_node = fdg_sim::Node::new(idx.index().to_string().as_str(), ()); @@ -280,7 +282,9 @@ impl ConfigurableApp { } fn add_edge(&mut self, start: NodeIndex, end: NodeIndex) { - self.g.g.add_edge(start, end, Edge::new(())); + let idx = self.g.g.add_edge(start, end, Edge::new(())); + let order = self.g.g.edges_connecting(start, end).count(); + self.g.g.edge_weight_mut(idx).unwrap().bind(idx, order); self.sim.get_graph_mut().add_edge(start, end, 1.); } @@ -303,10 +307,32 @@ impl ConfigurableApp { return; } + let order = self.g.g.edge_weight(g_idx.unwrap()).unwrap().order(); + self.g.g.remove_edge(g_idx.unwrap()).unwrap(); let sim_idx = self.sim.get_graph_mut().find_edge(start, end).unwrap(); self.sim.get_graph_mut().remove_edge(sim_idx).unwrap(); + + // update order of the edges + let left_siblings = self + .g + .g + .edges_connecting(start, end) + .map(|edge_ref| edge_ref.id()) + .collect::>(); + + left_siblings.iter().for_each(|idx| { + let sibling_order = self.g.g.edge_weight(*idx).unwrap().order(); + if sibling_order < order { + return; + } + self.g + .g + .edge_weight_mut(*idx) + .unwrap() + .set_order(sibling_order - 1); + }); } /// Removes all edges between two nodes @@ -396,12 +422,6 @@ impl ConfigurableApp { }); CollapsingHeader::new("Style").show(ui, |ui| { - ui.add(Slider::new(&mut self.settings_style.edge_radius_weight, 0. ..=5.) - .text("edge_radius_weight")); - ui.label("For every edge connected to node its radius is getting bigger by this value."); - - ui.add_space(5.); - ui.checkbox(&mut self.settings_style.labels_always, "labels_always"); ui.label("Wheter to show labels always or when interacted only."); }); @@ -551,25 +571,30 @@ impl App for ConfigurableApp { egui::CentralPanel::default().show(ctx, |ui| { let settings_interaction = &egui_graphs::SettingsInteraction::new() .with_node_selection_enabled(self.settings_interaction.node_selection_enabled) - .with_node_selection_multi_enabled(self.settings_interaction.node_selection_multi_enabled) + .with_node_selection_multi_enabled( + self.settings_interaction.node_selection_multi_enabled, + ) .with_dragging_enabled(self.settings_interaction.dragging_enabled) .with_node_clicking_enabled(self.settings_interaction.node_clicking_enabled) .with_edge_clicking_enabled(self.settings_interaction.edge_clicking_enabled) .with_edge_selection_enabled(self.settings_interaction.edge_selection_enabled) - .with_edge_selection_multi_enabled(self.settings_interaction.edge_selection_multi_enabled); + .with_edge_selection_multi_enabled( + self.settings_interaction.edge_selection_multi_enabled, + ); let settings_navigation = &egui_graphs::SettingsNavigation::new() .with_zoom_and_pan_enabled(self.settings_navigation.zoom_and_pan_enabled) .with_fit_to_screen_enabled(self.settings_navigation.fit_to_screen_enabled) .with_zoom_speed(self.settings_navigation.zoom_speed); let settings_style = &egui_graphs::SettingsStyle::new() - .with_labels_always(self.settings_style.labels_always) - .with_edge_radius_weight(self.settings_style.edge_radius_weight); + .with_labels_always(self.settings_style.labels_always); ui.add( - &mut GraphView::new(&mut self.g) - .with_interactions(settings_interaction) - .with_navigations(settings_navigation) - .with_styles(settings_style) - .with_events(&self.event_publisher), + &mut GraphView::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape<_>>::new( + &mut self.g, + ) + .with_interactions(settings_interaction) + .with_navigations(settings_navigation) + .with_styles(settings_style) + .with_events(&self.event_publisher), ); }); diff --git a/examples/configurable/src/settings.rs b/examples/configurable/src/settings.rs index e7e3ab55..863ee0a0 100644 --- a/examples/configurable/src/settings.rs +++ b/examples/configurable/src/settings.rs @@ -41,16 +41,7 @@ impl Default for SettingsNavigation { } } +#[derive(Default)] pub struct SettingsStyle { - pub edge_radius_weight: f32, pub labels_always: bool, } - -impl Default for SettingsStyle { - fn default() -> Self { - Self { - edge_radius_weight: 1., - labels_always: false, - } - } -} diff --git a/examples/custom_draw/src/main.rs b/examples/custom_draw/src/main.rs index 5b3e5a52..428e2b29 100644 --- a/examples/custom_draw/src/main.rs +++ b/examples/custom_draw/src/main.rs @@ -1,11 +1,14 @@ use eframe::{run_native, App, CreationContext}; -use egui::{epaint::TextShape, Context, FontFamily, FontId, Rect, Rounding, Shape, Stroke, Vec2}; -use egui_graphs::{default_edges_draw, Graph, GraphView, SettingsInteraction}; +use egui::Context; +use egui_graphs::{DefaultEdgeShape, Graph, GraphView, SettingsInteraction, SettingsNavigation}; +use node::NodeShape; use petgraph::{ stable_graph::{DefaultIx, StableGraph}, Directed, }; +mod node; + pub struct CustomDrawApp { g: Graph<(), (), Directed, DefaultIx>, } @@ -21,89 +24,18 @@ impl App for CustomDrawApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.add( - &mut GraphView::new(&mut self.g) + &mut GraphView::<_, _, _, _, NodeShape, DefaultEdgeShape<_>>::new(&mut self.g) + .with_navigations( + &SettingsNavigation::default() + .with_fit_to_screen_enabled(false) + .with_zoom_and_pan_enabled(true), + ) .with_interactions( &SettingsInteraction::default() .with_dragging_enabled(true) - .with_node_selection_enabled(true), - ) - .with_custom_node_draw(|ctx, n, state, l| { - // lets draw a rect with label in the center for every node - - // find node center location on the screen coordinates - let node_center_loc = n.screen_location(state.meta).to_pos2(); - - // find node radius accounting for current zoom level; we will use it as a reference for the rect and label sizes - let rad = n.screen_radius(state.meta, state.style); - - // first create rect shape - let size = Vec2::new(rad * 1.5, rad * 1.5); - let rect = Rect::from_center_size(node_center_loc, size); - let shape_rect = Shape::rect_stroke( - rect, - Rounding::default(), - Stroke::new(1., n.color(ctx)), - ); - - // add rect to the layers - l.add(shape_rect); - - // then create label - let color = ctx.style().visuals.text_color(); - let galley = ctx.fonts(|f| { - f.layout_no_wrap( - n.label(), - FontId::new(rad, FontFamily::Monospace), - color, - ) - }); - - // we need to offset label by half its size to place it in the center of the rect - let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); - - // create the shape and add it to the layers - let shape_label = TextShape::new(node_center_loc + offset, galley); - l.add(shape_label); - }) - .with_custom_edge_draw(|ctx, bounds, edges, state, l| { - // draw edges with labels in the middle - - // draw default edges - default_edges_draw(ctx, bounds, edges, state, l); - - // get start and end nodes - let n_start = state.g.node(bounds.0).unwrap(); - let n_end = state.g.node(bounds.1).unwrap(); - - // get start and end node locations - let loc_start = n_start.screen_location(state.meta); - let loc_end = n_end.screen_location(state.meta); - - // compute edge center location - let center_loc = (loc_start + loc_end) / 2.; - - // let label be the average of bound nodes sizes - let size = (n_start.screen_radius(state.meta, state.style) - + n_end.screen_radius(state.meta, state.style)) - / 2.; - - // create label - let color = ctx.style().visuals.text_color(); - let galley = ctx.fonts(|f| { - f.layout_no_wrap( - format!("{}->{}", n_start.label(), n_end.label()), - FontId::new(size, FontFamily::Monospace), - color, - ) - }); - - // we need to offset half the label size to place it in the center of the edge - let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); - - // create the shape and add it to the layers - let shape_label = TextShape::new((center_loc + offset).to_pos2(), galley); - l.add(shape_label); - }), + .with_node_selection_enabled(true) + .with_edge_selection_enabled(true), + ), ); }); } diff --git a/examples/custom_draw/src/node.rs b/examples/custom_draw/src/node.rs new file mode 100644 index 00000000..d5cad165 --- /dev/null +++ b/examples/custom_draw/src/node.rs @@ -0,0 +1,125 @@ +use egui::{epaint::TextShape, FontFamily, FontId, Pos2, Rect, Rounding, Shape, Stroke, Vec2}; +use egui_graphs::{DisplayNode, Graph, Interactable, Node}; +use petgraph::{stable_graph::IndexType, EdgeType}; + +pub struct NodeShape { + label: String, + loc: Pos2, + selected: bool, + dragged: bool, + + size: f32, +} + +impl From> for NodeShape { + fn from(node: Node) -> Self { + Self { + label: node.label(), + loc: node.location(), + selected: node.selected(), + dragged: node.dragged(), + + size: 30., + } + } +} + +impl Interactable for NodeShape { + fn is_inside>( + &self, + _g: &Graph, + pos: Pos2, + ) -> bool { + let rect = Rect::from_center_size(self.loc, Vec2::new(self.size, self.size)); + + rect.contains(pos) + } +} + +impl DisplayNode for NodeShape { + fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { + let margin = 5.0; + find_intersection(self.loc, self.size + margin, dir) + } + + fn shapes(&self, ctx: &egui_graphs::DrawContext) -> Vec { + // lets draw a rect with label in the center for every node + + // find node center location on the screen coordinates + let center = ctx.meta.canvas_to_screen_pos(self.loc); + let size = ctx.meta.canvas_to_screen_size(self.size); + let rect = Rect::from_center_size(center, Vec2::new(size, size)); + + let interacted = self.selected || self.dragged; + let rect_color = match interacted { + true => ctx.ctx.style().visuals.selection.bg_fill, + false => ctx.ctx.style().visuals.weak_text_color(), + }; + + let shape_rect = Shape::rect_stroke(rect, Rounding::default(), Stroke::new(1., rect_color)); + + // create label + let color = ctx.ctx.style().visuals.text_color(); + let galley = ctx.ctx.fonts(|f| { + f.layout_no_wrap( + self.label.clone(), + FontId::new(ctx.meta.canvas_to_screen_size(10.), FontFamily::Monospace), + color, + ) + }); + + // we need to offset label by half its size to place it in the center of the rect + let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.); + + // create the shape and add it to the layers + let shape_label = TextShape::new(rect.center() + offset, galley); + + vec![shape_rect, shape_label.into()] + } +} + +fn find_intersection(center: Pos2, size: f32, direction: Vec2) -> Pos2 { + // Determine the intersection side based on the direction + if direction.x.abs() > direction.y.abs() { + // Intersects left or right side + let x = if direction.x > 0.0 { + center.x + size / 2.0 + } else { + center.x - size / 2.0 + }; + let y = center.y + direction.y / direction.x * (x - center.x); + Pos2::new(x, y) + } else { + // Intersects top or bottom side + let y = if direction.y > 0.0 { + center.y + size / 2.0 + } else { + center.y - size / 2.0 + }; + let x = center.x + direction.x / direction.y * (y - center.y); + Pos2::new(x, y) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_intersection_right_side() { + let center = Pos2::new(0.0, 0.0); + let size = 10.; + let direction = Vec2::new(1.0, 0.0); // Directly to the right + let expected = Pos2::new(5.0, 0.0); + assert_eq!(find_intersection(center, size, direction), expected); + } + + #[test] + fn test_intersection_top_side() { + let center = Pos2::new(0.0, 0.0); + let size = 10.; + let direction = Vec2::new(0.0, 1.0); // Directly upwards + let expected = Pos2::new(0.0, 5.0); + assert_eq!(find_intersection(center, size, direction), expected); + } +} diff --git a/examples/interactive/src/main.rs b/examples/interactive/src/main.rs index 404c49ff..25076e25 100644 --- a/examples/interactive/src/main.rs +++ b/examples/interactive/src/main.rs @@ -1,6 +1,8 @@ use eframe::{run_native, App, CreationContext}; use egui::Context; -use egui_graphs::{Graph, GraphView, SettingsInteraction, SettingsStyle}; +use egui_graphs::{ + DefaultEdgeShape, DefaultNodeShape, Graph, GraphView, SettingsInteraction, SettingsStyle, +}; use petgraph::{ stable_graph::{DefaultIx, StableGraph}, Directed, @@ -30,9 +32,11 @@ impl App for InteractiveApp { .with_edge_selection_multi_enabled(true); let style_settings = &SettingsStyle::new().with_labels_always(true); ui.add( - &mut GraphView::new(&mut self.g) - .with_styles(style_settings) - .with_interactions(interaction_settings), + &mut GraphView::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape<_>>::new( + &mut self.g, + ) + .with_styles(style_settings) + .with_interactions(interaction_settings), ); }); } diff --git a/examples/undirected/src/main.rs b/examples/undirected/src/main.rs index 706305f1..63c90a79 100644 --- a/examples/undirected/src/main.rs +++ b/examples/undirected/src/main.rs @@ -1,6 +1,6 @@ use eframe::{run_native, App, CreationContext}; use egui::Context; -use egui_graphs::{Graph, GraphView}; +use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph, GraphView}; use petgraph::{ stable_graph::{DefaultIx, StableGraph, StableUnGraph}, Undirected, @@ -20,7 +20,14 @@ impl UndirectedApp { impl App for UndirectedApp { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { - ui.add(&mut GraphView::new(&mut self.g)); + ui.add(&mut GraphView::< + _, + _, + _, + _, + DefaultNodeShape, + DefaultEdgeShape<_>, + >::new(&mut self.g)); }); } } diff --git a/src/computed.rs b/src/computed.rs index eb0385ff..02e9e1ef 100644 --- a/src/computed.rs +++ b/src/computed.rs @@ -2,7 +2,7 @@ use egui::{Rect, Vec2}; use petgraph::graph::{EdgeIndex, IndexType}; use petgraph::{stable_graph::NodeIndex, EdgeType}; -use crate::{Graph, Node, SettingsStyle}; +use crate::{Graph, Node}; /// The struct stores selections, dragged node and computed elements states. #[derive(Debug, Clone)] @@ -13,7 +13,6 @@ pub struct ComputedState { min: Vec2, max: Vec2, - max_rad: f32, } impl Default for ComputedState @@ -29,7 +28,6 @@ where min: Vec2::new(f32::MAX, f32::MAX), max: Vec2::new(f32::MIN, f32::MIN), - max_rad: f32::MIN, } } } @@ -57,12 +55,7 @@ where } } - pub fn comp_iter_bounds(&mut self, n: &Node, settings: &SettingsStyle) { - let rad = n.radius() + n.num_connections() as f32 * settings.edge_radius_weight; - if rad > self.max_rad { - self.max_rad = rad; - } - + pub fn comp_iter_bounds(&mut self, n: &Node) { let loc = n.location(); if loc.x < self.min.x { self.min.x = loc.x; @@ -79,9 +72,7 @@ where } pub fn graph_bounds(&self) -> Rect { - let min = self.min - Vec2::new(self.max_rad, self.max_rad); - let max = self.max + Vec2::new(self.max_rad, self.max_rad); - Rect::from_min_max(min.to_pos2(), max.to_pos2()) + Rect::from_min_max(self.min.to_pos2(), self.max.to_pos2()) } } diff --git a/src/draw/custom.rs b/src/draw/custom.rs deleted file mode 100644 index 322c0b2f..00000000 --- a/src/draw/custom.rs +++ /dev/null @@ -1,42 +0,0 @@ -use egui::Context; -use petgraph::graph::IndexType; -use petgraph::{stable_graph::NodeIndex, EdgeType}; - -use crate::{Edge, Graph, Metadata, Node, SettingsStyle}; - -use super::Layers; - -/// Contains all the data about current widget state which is needed for custom drawing functions. -pub struct WidgetState<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> { - pub g: &'a Graph, - pub style: &'a SettingsStyle, - pub meta: &'a Metadata, -} - -/// Allows to fully customize what shape would be drawn for node. -/// The function is called for every node in the graph. -/// -/// Parameters: -/// - egui context, is needed for computing node props and styles; -/// - node reference, contains all node data; -/// - widget state with references to graph, style and metadata; -/// - when you create a shape, add it to the layers. -pub type FnCustomNodeDraw = - fn(&Context, n: &Node, &WidgetState, &mut Layers); - -/// Allows to fully customize what shape would be drawn for an edge. -/// The function is **called once for every node pair** which has edges connecting them. So make sure you have drawn all the edges which are passed to the function. -/// -/// Parameters: -/// - egui context, is needed for computing node props and styles; -/// - start node index and end node index; -/// - vector of edges, all edges between start and end nodes; -/// - widget state with references to graph, style and metadata; -/// - when you create a shape, add it to the layers. -pub type FnCustomEdgeDraw = fn( - &Context, - (NodeIndex, NodeIndex), - Vec<&Edge>, - &WidgetState, - &mut Layers, -); diff --git a/src/draw/default_edge.rs b/src/draw/default_edge.rs new file mode 100644 index 00000000..01296a87 --- /dev/null +++ b/src/draw/default_edge.rs @@ -0,0 +1,495 @@ +use std::f32::consts::PI; + +use egui::{ + epaint::{CubicBezierShape, QuadraticBezierShape}, + Color32, Pos2, Shape, Stroke, Vec2, +}; +use petgraph::{matrix_graph::Nullable, stable_graph::IndexType, EdgeType}; + +use crate::{draw::DrawContext, elements::EdgeID, DisplayNode, Edge, Graph, Node}; + +use super::{DisplayEdge, Interactable}; + +#[derive(Clone, Debug)] +pub struct DefaultEdgeShape { + pub edge_id: EdgeID, + + pub selected: bool, + + pub width: f32, + pub tip_size: f32, + pub tip_angle: f32, + pub curve_size: f32, + pub loop_size: f32, +} + +impl From> for DefaultEdgeShape { + fn from(edge: Edge) -> Self { + Self { + edge_id: edge.id(), + + selected: edge.selected(), + + width: 2., + tip_size: 15., + tip_angle: std::f32::consts::TAU / 30., + curve_size: 20., + loop_size: 3., + } + } +} + +impl DisplayEdge + for DefaultEdgeShape +{ + fn shapes>( + &self, + ctx: &DrawContext, + ) -> Vec { + let (idx_start, idx_end) = ctx.g.edge_endpoints(self.edge_id.idx).unwrap(); + let n_start = ctx.g.node(idx_start).unwrap(); + let n_end = ctx.g.node(idx_end).unwrap(); + + let style = match self.selected { + true => ctx.ctx.style().visuals.widgets.active, + false => ctx.ctx.style().visuals.widgets.inactive, + }; + let color = style.fg_stroke.color; + + if idx_start == idx_end { + // draw loop + let node_size = node_size::<_, _, _, _, Dn>(n_start); + let stroke = Stroke::new(self.width * ctx.meta.zoom, color); + return vec![shape_looped( + ctx.meta.canvas_to_screen_size(node_size), + ctx.meta.canvas_to_screen_pos(n_start.location()), + stroke, + self, + ) + .into()]; + } + + let dir = (n_end.location() - n_start.location()).normalized(); + let start_connector_point = Dn::from(n_start.clone()).closest_boundary_point(dir); + let end_connector_point = Dn::from(n_end.clone()).closest_boundary_point(-dir); + + let tip_end = end_connector_point; + + let edge_start = start_connector_point; + let edge_end = end_connector_point - self.tip_size * dir; + + let stroke_edge = Stroke::new(self.width * ctx.meta.zoom, color); + let stroke_tip = Stroke::new(0., color); + if self.edge_id.order == 0 { + // draw straight edge + + let line = Shape::line_segment( + [ + ctx.meta.canvas_to_screen_pos(edge_start), + ctx.meta.canvas_to_screen_pos(edge_end), + ], + stroke_edge, + ); + if !ctx.g.is_directed() { + return vec![line]; + } + + let tip_start_1 = tip_end - self.tip_size * rotate_vector(dir, self.tip_angle); + let tip_start_2 = tip_end - self.tip_size * rotate_vector(dir, -self.tip_angle); + + // draw tips for directed edges + + let line_tip = Shape::convex_polygon( + vec![ + ctx.meta.canvas_to_screen_pos(tip_end), + ctx.meta.canvas_to_screen_pos(tip_start_1), + ctx.meta.canvas_to_screen_pos(tip_start_2), + ], + color, + stroke_tip, + ); + return vec![line, line_tip]; + } + + // draw curved edge + + let dir_perpendicular = Vec2::new(-dir.y, dir.x); + let center_point = (edge_start + edge_end.to_vec2()).to_vec2() / 2.; + let control_point = (center_point + + dir_perpendicular * self.curve_size * self.edge_id.order as f32) + .to_pos2(); + + let tip_dir = (control_point - tip_end).normalized(); + + let arrow_tip_dir_1 = rotate_vector(tip_dir, self.tip_angle) * self.tip_size; + let arrow_tip_dir_2 = rotate_vector(tip_dir, -self.tip_angle) * self.tip_size; + + let tip_start_1 = tip_end + arrow_tip_dir_1; + let tip_start_2 = tip_end + arrow_tip_dir_2; + + let edge_end_curved = point_between(tip_start_1, tip_start_2); + + let line_curved = QuadraticBezierShape::from_points_stroke( + [ + ctx.meta.canvas_to_screen_pos(edge_start), + ctx.meta.canvas_to_screen_pos(control_point), + ctx.meta.canvas_to_screen_pos(edge_end_curved), + ], + false, + Color32::TRANSPARENT, + stroke_edge, + ); + + if !ctx.g.is_directed() { + return vec![line_curved.into()]; + } + + let line_curved_tip = Shape::convex_polygon( + vec![ + ctx.meta.canvas_to_screen_pos(tip_end), + ctx.meta.canvas_to_screen_pos(tip_start_1), + ctx.meta.canvas_to_screen_pos(tip_start_2), + ], + color, + stroke_tip, + ); + + vec![line_curved.into(), line_curved_tip] + } +} + +fn shape_looped( + node_size: f32, + node_center: Pos2, + stroke: Stroke, + e: &DefaultEdgeShape, +) -> CubicBezierShape { + let center_horizon_angle = PI / 4.; + let y_intersect = node_center.y - node_size * center_horizon_angle.sin(); + + let edge_start = Pos2::new( + node_center.x - node_size * center_horizon_angle.cos(), + y_intersect, + ); + let edge_end = Pos2::new( + node_center.x + node_size * center_horizon_angle.cos(), + y_intersect, + ); + + let loop_size = node_size * (e.loop_size + e.edge_id.order as f32); + + let control_point1 = Pos2::new(node_center.x + loop_size, node_center.y - loop_size); + let control_point2 = Pos2::new(node_center.x - loop_size, node_center.y - loop_size); + + CubicBezierShape::from_points_stroke( + [edge_end, control_point1, control_point2, edge_start], + false, + Color32::default(), + stroke, + ) +} + +fn shape_curved( + pos_start: Pos2, + pos_end: Pos2, + size_start: f32, + size_end: f32, + stroke: Stroke, + e: &DefaultEdgeShape, +) -> QuadraticBezierShape { + let vec = pos_end - pos_start; + let dist: f32 = vec.length(); + let dir = vec / dist; + + let start_node_radius_vec = Vec2::new(size_start, size_start) * dir; + let end_node_radius_vec = Vec2::new(size_end, size_end) * dir; + + let tip_end = pos_start + vec - end_node_radius_vec; + + let edge_start = pos_start + start_node_radius_vec; + let edge_end = pos_end + end_node_radius_vec; + + let dir_perpendicular = Vec2::new(-dir.y, dir.x); + let center_point = (edge_start + tip_end.to_vec2()).to_vec2() / 2.0; + let control_point = + (center_point + dir_perpendicular * e.curve_size * e.edge_id.order as f32).to_pos2(); + + QuadraticBezierShape::from_points_stroke( + [edge_start, control_point, edge_end], + false, + stroke.color, + stroke, + ) +} + +impl Interactable + for DefaultEdgeShape +{ + fn is_inside>( + &self, + g: &Graph, + pos: egui::Pos2, + ) -> bool { + let (idx_start, idx_end) = g.edge_endpoints(self.edge_id.idx).unwrap(); + let node_start = g.node(idx_start).unwrap(); + let node_end = g.node(idx_end).unwrap(); + + if idx_start == idx_end { + return is_inside_loop::<_, _, _, _, Nd>(node_start, self, pos); + } + + let pos_start = node_start.location(); + let pos_end = node_end.location(); + + if self.edge_id.order == 0 { + return is_inside_line(pos_start, pos_end, pos, self); + } + + is_inside_curve::(node_start, node_end, self, pos) + } +} + +fn is_inside_loop< + E: Clone, + N: Clone, + Ix: IndexType, + Ty: EdgeType, + Dn: DisplayNode, +>( + node: &Node, + e: &DefaultEdgeShape, + pos: Pos2, +) -> bool { + let node_size = node_size::<_, _, _, _, Dn>(node); + + let shape = shape_looped(node_size, node.location(), Stroke::default(), e); + is_point_on_cubic_bezier_curve(pos, shape, e.width) +} + +fn is_inside_line( + pos_start: Pos2, + pos_end: Pos2, + pos: Pos2, + e: &DefaultEdgeShape, +) -> bool { + let distance = distance_segment_to_point(pos_start, pos_end, pos); + distance <= e.width +} + +fn is_inside_curve< + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Dn: DisplayNode, +>( + node_start: &Node, + node_end: &Node, + e: &DefaultEdgeShape, + pos: Pos2, +) -> bool { + let pos_start = node_start.location(); + let pos_end = node_end.location(); + + let size_start = node_size::<_, _, _, _, Dn>(node_start); + let size_end = node_size::<_, _, _, _, Dn>(node_end); + + let shape = shape_curved( + pos_start, + pos_end, + size_start, + size_end, + Stroke::default(), + e, + ); + is_point_on_quadratic_bezier_curve(pos, shape, e.width) +} + +fn node_size>( + node: &Node, +) -> f32 { + let left_dir = Vec2::new(-1., 0.); + let connector_left = Dn::from(node.clone()).closest_boundary_point(left_dir); + let connector_right = Dn::from(node.clone()).closest_boundary_point(-left_dir); + + (connector_right.x - connector_left.x) / 2. +} + +/// Returns the distance from line segment `a``b` to point `c`. +/// Adapted from https://stackoverflow.com/questions/1073336/circle-line-segment-collision-detection-algorithm +fn distance_segment_to_point(a: Pos2, b: Pos2, point: Pos2) -> f32 { + let ac = point - a; + let ab = b - a; + + let d = a + proj(ac, ab); + + let ad = d - a; + + let k = if ab.x.abs() > ab.y.abs() { + ad.x / ab.x + } else { + ad.y / ab.y + }; + + if k <= 0.0 { + return hypot2(point.to_vec2(), a.to_vec2()).sqrt(); + } else if k >= 1.0 { + return hypot2(point.to_vec2(), b.to_vec2()).sqrt(); + } + + hypot2(point.to_vec2(), d.to_vec2()).sqrt() +} + +/// Calculates the square of the Euclidean distance between vectors `a` and `b`. +fn hypot2(a: Vec2, b: Vec2) -> f32 { + (a - b).dot(a - b) +} + +/// Calculates the projection of vector `a` onto vector `b`. +fn proj(a: Vec2, b: Vec2) -> Vec2 { + let k = a.dot(b) / b.dot(b); + Vec2::new(k * b.x, k * b.y) +} + +fn is_point_on_cubic_bezier_curve(point: Pos2, curve: CubicBezierShape, width: f32) -> bool { + is_point_on_bezier_curve(point, curve.flatten(Option::new(10.0)), width) +} + +fn is_point_on_quadratic_bezier_curve( + point: Pos2, + curve: QuadraticBezierShape, + width: f32, +) -> bool { + is_point_on_bezier_curve(point, curve.flatten(Option::new(0.3)), width) +} + +fn is_point_on_bezier_curve(point: Pos2, curve_points: Vec, width: f32) -> bool { + let mut previous_point = None; + for p in curve_points { + if let Some(pp) = previous_point { + let distance = distance_segment_to_point(p, pp, point); + if distance < width { + return true; + } + } + previous_point = Some(p); + } + false +} + +/// rotates vector by angle +fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { + let cos = angle.cos(); + let sin = angle.sin(); + Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) +} + +/// finds point exactly in the middle between 2 points +fn point_between(p1: Pos2, p2: Pos2) -> Pos2 { + let base = p1 - p2; + let base_len = base.length(); + let dir = base / base_len; + p1 - (base_len / 2.) * dir +} + +// TODO: check test cases +#[cfg(test)] +mod tests { + use egui::{Color32, Stroke}; + + use super::*; + + #[test] + fn test_distance_segment_to_point() { + let segment_1 = Pos2::new(2.0, 2.0); + let segment_2 = Pos2::new(2.0, 5.0); + let point = Pos2::new(4.0, 3.0); + assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 2.0); + } + + #[test] + fn test_distance_segment_to_point_on_segment() { + let segment_1 = Pos2::new(1.0, 2.0); + let segment_2 = Pos2::new(1.0, 5.0); + let point = Pos2::new(1.0, 3.0); + assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 0.0); + } + + #[test] + fn test_hypot2() { + let a = Vec2::new(0.0, 1.0); + let b = Vec2::new(0.0, 5.0); + assert_eq!(hypot2(a, b), 16.0); + } + + #[test] + fn test_hypot2_no_distance() { + let a = Vec2::new(0.0, 1.0); + assert_eq!(hypot2(a, a), 0.0); + } + + #[test] + fn test_proj() { + let a = Vec2::new(5.0, 8.0); + let b = Vec2::new(10.0, 0.0); + let result = proj(a, b); + assert_eq!(result.x, 5.0); + assert_eq!(result.y, 0.0); + } + + #[test] + fn test_proj_orthogonal() { + let a = Vec2::new(5.0, 0.0); + let b = Vec2::new(0.0, 5.0); + let result = proj(a, b); + assert_eq!(result.x, 0.0); + assert_eq!(result.y, 0.0); + } + + #[test] + fn test_proj_same_vector() { + let a = Vec2::new(5.3, 4.9); + assert_eq!(proj(a, a), a); + } + + #[test] + fn test_is_point_on_cubic_bezier_curve() { + let edge_start = Pos2::new(-3.0, 0.0); + let edge_end = Pos2::new(3.0, 0.0); + let control_point1 = Pos2::new(-3.0, 3.0); + let control_point2 = Pos2::new(4.0, 2.0); + let curve = CubicBezierShape::from_points_stroke( + [edge_end, control_point1, control_point2, edge_start], + false, + Color32::default(), + Stroke::default(), + ); + + let width = 1.0; + let p1 = Pos2::new(0.0, 2.0); + assert!(!is_point_on_cubic_bezier_curve(p1, curve, width)); + + let p2 = Pos2::new(2.0, 1.0); + assert!(!is_point_on_cubic_bezier_curve(p2, curve, width)); + } + + #[test] + fn test_is_point_on_quadratic_bezier_curve() { + let edge_start = Pos2::new(0.0, 0.0); + let edge_end = Pos2::new(20.0, 0.0); + let control_point = Pos2::new(10.0, 8.0); + let curve = QuadraticBezierShape::from_points_stroke( + [edge_start, control_point, edge_end], + false, + Color32::default(), + Stroke::default(), + ); + + let width = 1.0; + let p1 = Pos2::new(10.0, 4.0); + assert!(is_point_on_quadratic_bezier_curve(p1, curve, width)); + + let p2 = Pos2::new(3.0, 2.0); + assert!(is_point_on_quadratic_bezier_curve(p2, curve, width)); + } +} diff --git a/src/draw/default_node.rs b/src/draw/default_node.rs new file mode 100644 index 00000000..3bf7eb19 --- /dev/null +++ b/src/draw/default_node.rs @@ -0,0 +1,152 @@ +use egui::{ + epaint::{CircleShape, TextShape}, + FontFamily, FontId, Pos2, Shape, Stroke, Vec2, +}; +use petgraph::{stable_graph::IndexType, EdgeType}; + +use crate::{draw::drawer::DrawContext, Graph, Node}; + +use super::{DisplayNode, Interactable}; + +/// This is the default node shape which is used to display nodes in the graph. +/// +/// You can use this implementation as an example for implementing your own custom node shapes. +#[derive(Clone, Debug)] +pub struct DefaultNodeShape { + pub pos: Pos2, + + pub selected: bool, + pub dragged: bool, + + pub label_text: String, + + /// Shape defined property + pub radius: f32, +} + +impl From> for DefaultNodeShape { + fn from(node: Node) -> Self { + DefaultNodeShape { + pos: node.location(), + + selected: node.selected(), + dragged: node.dragged(), + + label_text: node.label().to_string(), + + radius: 5.0, + } + } +} + +impl Interactable + for DefaultNodeShape +{ + fn is_inside>( + &self, + _g: &Graph, + pos: Pos2, + ) -> bool { + is_inside_circle(self.pos, self.radius, pos) + } +} + +impl DisplayNode + for DefaultNodeShape +{ + fn closest_boundary_point(&self, dir: Vec2) -> Pos2 { + closest_point_on_circle(self.pos, self.radius, dir) + } + + fn shapes(&self, ctx: &DrawContext) -> Vec { + let mut res = Vec::with_capacity(2); + + let is_interacted = self.selected || self.dragged; + + let style = match is_interacted { + true => ctx.ctx.style().visuals.widgets.active, + false => ctx.ctx.style().visuals.widgets.inactive, + }; + let color = style.fg_stroke.color; + + let circle_center = ctx.meta.canvas_to_screen_pos(self.pos); + let circle_radius = ctx.meta.canvas_to_screen_size(self.radius); + let circle_shape = CircleShape { + center: circle_center, + radius: circle_radius, + fill: color, + stroke: Stroke::default(), + }; + res.push(circle_shape.into()); + + let label_visible = ctx.style.labels_always || self.selected || self.dragged; + if !label_visible { + return res; + } + + // display label centered over the circle + let label_pos = Pos2::new(circle_center.x, circle_center.y - circle_radius * 2.); + let galley = ctx.ctx.fonts(|f| { + f.layout_no_wrap( + self.label_text.clone(), + FontId::new(circle_radius, FontFamily::Monospace), + color, + ) + }); + + let label_shape = TextShape::new(label_pos, galley); + res.push(label_shape.into()); + + res + } +} + +fn closest_point_on_circle(center: Pos2, radius: f32, dir: Vec2) -> Pos2 { + center + dir.normalized() * radius +} + +fn is_inside_circle(center: Pos2, radius: f32, pos: Pos2) -> bool { + let dir = pos - center; + dir.length() <= radius +} + +#[cfg(test)] +mod test { + use super::*; + use egui::Pos2; + + #[test] + fn test_closest_point_on_circle() { + assert_eq!( + closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(5.0, 0.0)), + Pos2::new(10.0, 0.0) + ); + assert_eq!( + closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(15.0, 0.0)), + Pos2::new(10.0, 0.0) + ); + assert_eq!( + closest_point_on_circle(Pos2::new(0.0, 0.0), 10.0, Vec2::new(0.0, 10.0)), + Pos2::new(0.0, 10.0) + ); + } + + #[test] + fn test_is_inside_circle() { + assert!(is_inside_circle( + Pos2::new(0.0, 0.0), + 10.0, + Pos2::new(5.0, 0.0) + )); + assert!(!is_inside_circle( + Pos2::new(0.0, 0.0), + 10.0, + Pos2::new(15.0, 0.0) + )); + assert!(is_inside_circle( + Pos2::new(0.0, 0.0), + 10.0, + Pos2::new(0.0, 10.0) + )); + } +} diff --git a/src/draw/displays.rs b/src/draw/displays.rs new file mode 100644 index 00000000..a7fccf2a --- /dev/null +++ b/src/draw/displays.rs @@ -0,0 +1,52 @@ +use egui::{Pos2, Shape, Vec2}; +use petgraph::{stable_graph::IndexType, EdgeType}; + +use crate::{draw::drawer::DrawContext, Edge, Graph, Node}; + +pub trait DisplayNode: Interactable + From> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, +{ + /// Returns the closest point on the shape boundary in the direction of dir. + /// + /// * `dir` - direction pointing from the shape center to the required boundary point. + /// + /// Could be used to snap the edge ends to the node. + fn closest_boundary_point(&self, dir: Vec2) -> Pos2; + + /// Draws shapes of the node. + /// + /// * `ctx` - should be used to determine current global properties. + /// + /// Use `ctx.meta` to properly scale and translate the shape. + fn shapes(&self, ctx: &DrawContext) -> Vec; +} +pub trait DisplayEdge: Interactable + From> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, +{ + /// Draws shapes of the edge. + /// + /// * `ctx` - should be used to determine current global properties. + /// * `start` and `end` - start and end points of the edge. + /// + /// Use `ctx.meta` to properly scale and translate the shape. + /// + /// Get [NodeGraphDisplay] from node endpoints to get start and end coordinates using [closest_boundary_point](NodeGraphDisplay::closest_boundary_point). + fn shapes>(&self, ctx: &DrawContext) -> Vec; +} + +pub trait Interactable { + /// Checks if the provided `pos` is inside the shape. + /// + /// * `pos` - position is in the canvas coordinates. + /// + /// Could be used to bind mouse events to the custom drawn nodes. + fn is_inside>(&self, g: &Graph, pos: Pos2) -> bool; +} diff --git a/src/draw/drawer.rs b/src/draw/drawer.rs index 346c1269..2f464629 100644 --- a/src/draw/drawer.rs +++ b/src/draw/drawer.rs @@ -1,47 +1,50 @@ -use std::collections::HashMap; +use std::marker::PhantomData; -use egui::Painter; +use egui::{Context, Painter}; use petgraph::graph::IndexType; -use petgraph::{stable_graph::NodeIndex, EdgeType}; +use petgraph::EdgeType; -use crate::{settings::SettingsStyle, Edge, Graph, Metadata}; +use crate::{settings::SettingsStyle, Graph, Metadata}; -use super::{ - custom::{FnCustomEdgeDraw, FnCustomNodeDraw, WidgetState}, - default_edges_draw, default_node_draw, - layers::Layers, -}; +use super::layers::Layers; +use super::{DisplayEdge, DisplayNode}; -/// Mapping for 2 nodes and all edges between them -type EdgeMap<'a, E, Ix> = HashMap<(NodeIndex, NodeIndex), Vec<&'a Edge>>; +/// Contains all the data about current widget state which is needed for custom drawing functions. +pub struct DrawContext<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> { + pub ctx: &'a Context, + pub g: &'a Graph, + pub style: &'a SettingsStyle, + pub meta: &'a Metadata, +} -pub struct Drawer<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> { +pub struct Drawer<'a, N, E, Ty, Ix, Nd, Ed> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Nd: DisplayNode, + Ed: DisplayEdge, +{ p: Painter, - - g: &'a Graph, - style: &'a SettingsStyle, - meta: &'a Metadata, - - custom_node_draw: Option>, - custom_edge_draw: Option>, + ctx: &'a DrawContext<'a, N, E, Ty, Ix>, + _marker: PhantomData<(Nd, Ed)>, } -impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> Drawer<'a, N, E, Ty, Ix> { - pub fn new( - p: Painter, - g: &'a Graph, - style: &'a SettingsStyle, - meta: &'a Metadata, - custom_node_draw: Option>, - custom_edge_draw: Option>, - ) -> Self { +impl<'a, N, E, Ty, Ix, Nd, Ed> Drawer<'a, N, E, Ty, Ix, Nd, Ed> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Nd: DisplayNode, + Ed: DisplayEdge, +{ + pub fn new(p: Painter, ctx: &'a DrawContext<'a, N, E, Ty, Ix>) -> Self { Drawer { - g, p, - style, - meta, - custom_node_draw, - custom_edge_draw, + ctx, + _marker: PhantomData, } } @@ -55,39 +58,22 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> Drawer<'a, N, E, Ty, I } fn fill_layers_nodes(&self, l: &mut Layers) { - let state = &WidgetState { - g: self.g, - meta: self.meta, - style: self.style, - }; - self.g - .nodes_iter() - .for_each(|(_, n)| match self.custom_node_draw { - Some(f) => f(self.p.ctx(), n, state, l), - None => default_node_draw(self.p.ctx(), n, state, l), - }); + self.ctx.g.nodes_iter().for_each(|(_, n)| { + let shapes = Nd::from(n.clone().clone()).shapes(self.ctx); + match n.selected() || n.dragged() { + true => shapes.into_iter().for_each(|s| l.add_top(s)), + false => shapes.into_iter().for_each(|s| l.add(s)), + } + }); } fn fill_layers_edges(&self, l: &mut Layers) { - let mut edge_map: EdgeMap = HashMap::new(); - - self.g.edges_iter().for_each(|(idx, e)| { - let (source, target) = self.g.edge_endpoints(idx).unwrap(); - // compute map with edges between 2 nodes - edge_map.entry((source, target)).or_default().push(e); + self.ctx.g.edges_iter().for_each(|(_, e)| { + let shapes = Ed::from(e.clone().clone()).shapes::(self.ctx); + match e.selected() { + true => shapes.into_iter().for_each(|s| l.add_top(s)), + false => shapes.into_iter().for_each(|s| l.add(s)), + } }); - - let state = &WidgetState { - g: self.g, - meta: self.meta, - style: self.style, - }; - - edge_map - .into_iter() - .for_each(|((start, end), edges)| match self.custom_edge_draw { - Some(f) => f(self.p.ctx(), (start, end), edges, state, l), - None => default_edges_draw(self.p.ctx(), (start, end), edges, state, l), - }); } } diff --git a/src/draw/edge.rs b/src/draw/edge.rs deleted file mode 100644 index c61a68c9..00000000 --- a/src/draw/edge.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::f32::consts::PI; - -use egui::{ - epaint::{CubicBezierShape, QuadraticBezierShape}, - Color32, Context, Pos2, Shape, Stroke, Vec2, -}; -use petgraph::graph::IndexType; -use petgraph::{stable_graph::NodeIndex, EdgeType}; - -use crate::{Edge, Node}; - -use super::{custom::WidgetState, Layers}; - -pub fn default_edges_draw( - ctx: &Context, - bounds: (NodeIndex, NodeIndex), - edges: Vec<&Edge>, - state: &WidgetState, - l: &mut Layers, -) { - let (idx_start, idx_end) = bounds; - let mut order = edges.len(); - edges.iter().for_each(|e| { - let n_start = state.g.node(idx_start).unwrap(); - let n_end = state.g.node(idx_end).unwrap(); - - order -= 1; - - if idx_start == idx_end { - draw_edge_looped(ctx, l, n_start, e, order, state); - } else { - draw_edge_basic(ctx, l, n_start, n_end, e, order, state); - } - }); -} - -fn draw_edge_basic( - ctx: &Context, - l: &mut Layers, - n_start: &Node, - n_end: &Node, - e: &Edge, - order: usize, - state: &WidgetState, -) { - let loc_start = n_start.screen_location(state.meta).to_pos2(); - let loc_end = n_end.screen_location(state.meta).to_pos2(); - let rad_start = n_start.screen_radius(state.meta, state.style); - let rad_end = n_end.screen_radius(state.meta, state.style); - - let vec = loc_end - loc_start; - let dist: f32 = vec.length(); - let dir = vec / dist; - - let start_node_radius_vec = Vec2::new(rad_start, rad_start) * dir; - let end_node_radius_vec = Vec2::new(rad_end, rad_end) * dir; - - let tip_end = loc_start + vec - end_node_radius_vec; - - let edge_start = loc_start + start_node_radius_vec; - let edge_end = match state.g.is_directed() { - true => tip_end - e.tip_size() * state.meta.zoom * dir, - false => tip_end, - }; - - let color = e.color(ctx); - let stroke_edge = Stroke::new(e.width() * state.meta.zoom, color); - let stroke_tip = Stroke::new(0., color); - - // draw straight edge - if order == 0 { - let tip_start_1 = - tip_end - e.tip_size() * state.meta.zoom * rotate_vector(dir, e.tip_angle()); - let tip_start_2 = - tip_end - e.tip_size() * state.meta.zoom * rotate_vector(dir, -e.tip_angle()); - - let shape = Shape::line_segment([edge_start, edge_end], stroke_edge); - match e.selected() { - true => l.add_top(shape), - false => l.add(shape), - } - - if !state.g.is_directed() { - return; - } - - // draw tips for directed edges - let shape_tip = - Shape::convex_polygon(vec![tip_end, tip_start_1, tip_start_2], color, stroke_tip); - match e.selected() { - true => l.add_top(shape_tip), - false => l.add(shape_tip), - }; - - return; - } - - // draw curved edge - let dir_perpendicular = Vec2::new(-dir.y, dir.x); - let center_point = (edge_start + edge_end.to_vec2()).to_vec2() / 2.0; - let control_point = (center_point - + dir_perpendicular * e.curve_size() * state.meta.zoom * order as f32) - .to_pos2(); - - let tip_vec = control_point - tip_end; - let tip_dir = tip_vec / tip_vec.length(); - let tip_size = e.tip_size() * state.meta.zoom; - - let arrow_tip_dir_1 = rotate_vector(tip_dir, e.tip_angle()) * tip_size; - let arrow_tip_dir_2 = rotate_vector(tip_dir, -e.tip_angle()) * tip_size; - - let tip_start_1 = tip_end + arrow_tip_dir_1; - let tip_start_2 = tip_end + arrow_tip_dir_2; - - let edge_end_curved = point_between(tip_start_1, tip_start_2); - - // draw curved not selected - let shape_curved = QuadraticBezierShape::from_points_stroke( - [edge_start, control_point, edge_end_curved], - false, - Color32::TRANSPARENT, - stroke_edge, - ); - - match e.selected() { - true => l.add_top(shape_curved), - false => l.add(shape_curved), - } - - let shape_tip_curved = - Shape::convex_polygon(vec![tip_end, tip_start_1, tip_start_2], color, stroke_tip); - match e.selected() { - true => l.add_top(shape_tip_curved), - false => l.add(shape_tip_curved), - }; -} - -fn draw_edge_looped( - ctx: &Context, - l: &mut Layers, - node: &Node, - e: &Edge, - order: usize, - state: &WidgetState, -) { - let rad = node.screen_radius(state.meta, state.style); - let center = node.screen_location(state.meta); - let center_horizon_angle = PI / 4.; - let y_intersect = center.y - rad * center_horizon_angle.sin(); - - let edge_start = Pos2::new(center.x - rad * center_horizon_angle.cos(), y_intersect); - let edge_end = Pos2::new(center.x + rad * center_horizon_angle.cos(), y_intersect); - - let loop_size = rad * (state.style.edge_looped_size + order as f32); - - let control_point1 = Pos2::new(center.x + loop_size, center.y - loop_size); - let control_point2 = Pos2::new(center.x - loop_size, center.y - loop_size); - - let stroke = Stroke::new(e.width() * state.meta.zoom, e.color(ctx)); - let shape = CubicBezierShape::from_points_stroke( - [edge_end, control_point1, control_point2, edge_start], - false, - Color32::TRANSPARENT, - stroke, - ); - - match e.selected() { - true => l.add_top(shape), - false => l.add(shape), - } -} - -/// rotates vector by angle -fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { - let cos = angle.cos(); - let sin = angle.sin(); - Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) -} - -/// finds point exactly in the middle between 2 points -fn point_between(p1: Pos2, p2: Pos2) -> Pos2 { - let base = p1 - p2; - let base_len = base.length(); - let dir = base / base_len; - p1 - (base_len / 2.) * dir -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rotate_vector() { - let vec = Vec2::new(1.0, 0.0); - let angle = PI / 2.0; - let rotated = rotate_vector(vec, angle); - assert!((rotated.x - 0.0).abs() < 1e-6); - assert!((rotated.y - 1.0).abs() < 1e-6); - } - - #[test] - fn test_point_between() { - let m = point_between(Pos2::new(0.0, 0.0), Pos2::new(2.0, 0.0)); - assert!((m.x - 1.0).abs() < 1e-6); - assert!((m.y).abs() < 1e-6); - } -} diff --git a/src/draw/mod.rs b/src/draw/mod.rs index b1c1997b..37c71cf6 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -1,11 +1,11 @@ -mod custom; +mod default_edge; +mod default_node; +mod displays; mod drawer; -mod edge; mod layers; -mod node; -pub use self::custom::{FnCustomEdgeDraw, FnCustomNodeDraw}; -pub use self::drawer::Drawer; -pub use self::edge::default_edges_draw; +pub use self::drawer::{DrawContext, Drawer}; pub use self::layers::Layers; -pub use self::node::default_node_draw; +pub use default_edge::DefaultEdgeShape; +pub use default_node::DefaultNodeShape; +pub use displays::{DisplayEdge, DisplayNode, Interactable}; diff --git a/src/elements/edge.rs b/src/elements/edge.rs index 650ac7fc..c6ee0ce3 100644 --- a/src/elements/edge.rs +++ b/src/elements/edge.rs @@ -1,48 +1,76 @@ -use super::StyleEdge; use egui::{Color32, Context}; +use petgraph::stable_graph::{EdgeIndex, IndexType}; + +/// Uniquely identifies edge with source, target and index in the set of duplicate edges. +#[derive(Clone, Debug)] +pub struct EdgeID { + pub idx: EdgeIndex, + + /// Index of the edge among siblings. + pub order: usize, +} + +impl EdgeID { + pub fn new(idx: EdgeIndex) -> Self { + Self { + idx, + order: Default::default(), + } + } + + pub fn with_order(mut self, order: usize) -> Self { + self.order = order; + self + } +} /// Stores properties of an edge that can be changed. Used to apply changes to the graph. #[derive(Clone, Debug)] -pub struct Edge { - /// Client data - data: Option, +pub struct Edge { + id: Option>, - style: StyleEdge, + /// Client data + payload: Option, selected: bool, } -impl Default for Edge { - fn default() -> Self { +impl Edge { + pub fn new(payload: E) -> Self { Self { - style: Default::default(), - - data: Default::default(), + payload: Some(payload), + id: Default::default(), selected: Default::default(), } } -} -impl Edge { - pub fn new(data: E) -> Self { - Self { - data: Some(data), + /// Binds node to the actual node and position in the graph. + pub fn bind(&mut self, idx: EdgeIndex, order: usize) { + let id = EdgeID::new(idx).with_order(order); + self.id = Some(id); + } - ..Default::default() - } + pub fn id(&self) -> EdgeID { + self.id.clone().unwrap() } - pub fn tip_angle(&self) -> f32 { - self.style.tip_angle + // TODO: handle unwrap + pub fn order(&self) -> usize { + self.id.as_ref().unwrap().order } - pub fn data(&self) -> Option<&E> { - self.data.as_ref() + // TODO: handle unwrap + pub fn set_order(&mut self, order: usize) { + self.id.as_mut().unwrap().order = order; } - pub fn data_mut(&mut self) -> Option<&mut E> { - self.data.as_mut() + pub fn payload(&self) -> Option<&E> { + self.payload.as_ref() + } + + pub fn payload_mut(&mut self) -> Option<&mut E> { + self.payload.as_mut() } pub fn color(&self, ctx: &Context) -> Color32 { if self.selected { @@ -54,24 +82,6 @@ impl Edge { .gray_out(ctx.style().visuals.widgets.inactive.fg_stroke.color) } - pub fn width(&self) -> f32 { - self.style.width - } - - pub fn with_width(&mut self, width: f32) -> Self { - let mut ne = self.clone(); - ne.style.width = width; - ne - } - - pub fn curve_size(&self) -> f32 { - self.style.curve_size - } - - pub fn tip_size(&self) -> f32 { - self.style.tip_size - } - pub fn set_selected(&mut self, selected: bool) { self.selected = selected; } diff --git a/src/elements/edge_style.rs b/src/elements/edge_style.rs deleted file mode 100644 index 7f6715e3..00000000 --- a/src/elements/edge_style.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[derive(Clone, Debug)] -pub struct StyleEdge { - pub width: f32, - pub tip_size: f32, - pub tip_angle: f32, - pub curve_size: f32, -} - -impl Default for StyleEdge { - fn default() -> Self { - Self { - width: 2., - tip_size: 15., - tip_angle: std::f32::consts::TAU / 30., - curve_size: 20., - } - } -} diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 48844179..a7ad364d 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -1,9 +1,5 @@ mod edge; -mod edge_style; mod node; -mod node_style; -pub use self::edge::Edge; -pub use self::edge_style::StyleEdge; +pub use self::edge::{Edge, EdgeID}; pub use self::node::Node; -pub use self::node_style::StyleNode; diff --git a/src/elements/node.rs b/src/elements/node.rs index b949c107..42cbab72 100644 --- a/src/elements/node.rs +++ b/src/elements/node.rs @@ -1,32 +1,29 @@ -use egui::{Color32, Context, Vec2}; +use egui::Pos2; +use petgraph::stable_graph::{IndexType, NodeIndex}; -use crate::{metadata::Metadata, ComputedNode, SettingsStyle}; - -use super::StyleNode; +use crate::ComputedNode; /// Stores properties of a node. #[derive(Clone, Debug)] -pub struct Node { - /// Client data - data: Option, - - location: Vec2, +pub struct Node { + id: Option>, + location: Option, + payload: Option, label: String, - style: StyleNode, - selected: bool, dragged: bool, computed: ComputedNode, } -impl Node { - pub fn new(location: Vec2, data: N) -> Self { +impl Node { + pub fn new(payload: N) -> Self { Self { - location, - data: Some(data), - style: Default::default(), + payload: Some(payload), + + id: Default::default(), + location: Default::default(), label: Default::default(), selected: Default::default(), dragged: Default::default(), @@ -34,56 +31,49 @@ impl Node { } } - /// Returns actual location of the node on the screen. It accounts for the current zoom and pan values. - pub fn screen_location(&self, m: &Metadata) -> Vec2 { - self.location * m.zoom + m.pan + /// Binds node to the actual node and position in the graph. + pub fn bind(&mut self, id: NodeIndex, location: Pos2) { + self.id = Some(id); + self.location = Some(location); } - /// Returns actual radius of the node on the screen. It accounts for the number of connections and current zoom value. - pub fn screen_radius(&self, m: &Metadata, style: &SettingsStyle) -> f32 { - (self.radius() + self.num_connections() as f32 * style.edge_radius_weight) * m.zoom - } - - pub fn radius(&self) -> f32 { - self.style.radius + pub fn id(&self) -> NodeIndex { + self.id.unwrap() } pub fn num_connections(&self) -> usize { self.computed.num_connections } - pub fn set_radius(&mut self, new_rad: f32) { - self.style.radius = new_rad - } - pub(crate) fn set_computed(&mut self, comp: ComputedNode) { self.computed = comp; } - pub fn data(&self) -> Option<&N> { - self.data.as_ref() + pub fn payload(&self) -> Option<&N> { + self.payload.as_ref() } - pub fn data_mut(&mut self) -> Option<&mut N> { - self.data.as_mut() + pub fn payload_mut(&mut self) -> Option<&mut N> { + self.payload.as_mut() } pub fn set_data(&mut self, data: Option) { - self.data = data; + self.payload = data; } pub fn with_data(&self, data: Option) -> Self { let mut res = self.clone(); - res.data = data; + res.payload = data; res } - pub fn location(&self) -> Vec2 { - self.location + // TODO: handle unbinded node + pub fn location(&self) -> Pos2 { + self.location.unwrap() } - pub fn set_location(&mut self, loc: Vec2) { - self.location = loc + pub fn set_location(&mut self, loc: Pos2) { + self.location = Some(loc) } pub fn selected(&self) -> bool { @@ -111,16 +101,4 @@ impl Node { res.label = label; res } - - pub fn color(&self, ctx: &Context) -> Color32 { - if self.dragged { - return ctx.style().visuals.widgets.active.fg_stroke.color; - } - - if self.selected { - return ctx.style().visuals.widgets.hovered.fg_stroke.color; - } - - ctx.style().visuals.widgets.inactive.fg_stroke.color - } } diff --git a/src/elements/node_style.rs b/src/elements/node_style.rs deleted file mode 100644 index 34033652..00000000 --- a/src/elements/node_style.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[derive(Clone, Debug)] -pub struct StyleNode { - pub radius: f32, -} - -impl Default for StyleNode { - fn default() -> Self { - Self { radius: 5. } - } -} diff --git a/src/graph.rs b/src/graph.rs index 87fdb4aa..22ff7c41 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,29 +1,21 @@ -use egui::epaint::{CubicBezierShape, QuadraticBezierShape}; -use egui::{Color32, Pos2, Stroke, Vec2}; +use egui::Pos2; use petgraph::stable_graph::DefaultIx; use petgraph::Directed; -use std::collections::HashMap; -use std::f32::consts::PI; -use std::ops::Index; use petgraph::graph::IndexType; -use petgraph::matrix_graph::Nullable; use petgraph::{ stable_graph::{EdgeIndex, EdgeReference, NodeIndex, StableGraph}, visit::{EdgeRef, IntoEdgeReferences, IntoNodeReferences}, Direction, EdgeType, }; -use crate::{metadata::Metadata, transform, Edge, Node, SettingsStyle}; - -/// Mapping for 2 nodes and all edges between them -pub type EdgeMap<'a, E, Ix> = - HashMap<(NodeIndex, NodeIndex), Vec<(EdgeIndex, &'a Edge)>>; +use crate::draw::{DisplayEdge, DisplayNode}; +use crate::{metadata::Metadata, transform, Edge, Node}; /// Graph type compatible with [`super::GraphView`]. #[derive(Debug, Clone)] pub struct Graph { - pub g: StableGraph, Edge, Ty, Ix>, + pub g: StableGraph, Edge, Ty, Ix>, } impl From<&StableGraph> @@ -34,155 +26,61 @@ impl From<&StableGraph Graph { - pub fn new(g: StableGraph, Edge, Ty, Ix>) -> Self { +impl Graph { + pub fn new(g: StableGraph, Edge, Ty, Ix>) -> Self { Self { g } } /// Finds node by position. Can be optimized by using a spatial index like quad-tree if needed. - pub fn node_by_screen_pos( + pub fn node_by_screen_pos>( &self, - meta: &'a Metadata, - style: &'a SettingsStyle, + meta: &Metadata, screen_pos: Pos2, - ) -> Option<(NodeIndex, &Node)> { - let pos_in_graph = (screen_pos.to_vec2() - meta.pan) / meta.zoom; - self.nodes_iter().find(|(_, n)| { - let dist_to_node = (n.location() - pos_in_graph).length(); - dist_to_node <= n.screen_radius(meta, style) / meta.zoom - }) + ) -> Option<(NodeIndex, &Node)> { + let pos_in_graph = meta.screen_to_canvas_pos(screen_pos); + for (idx, node) in self.nodes_iter() { + if D::from(node.clone()).is_inside::(self, pos_in_graph) { + return Some((idx, node)); + } + } + None } /// Finds edge by position. - pub fn edge_by_screen_pos( + pub fn edge_by_screen_pos, Dn: DisplayNode>( &self, - meta: &'a Metadata, - style: &'a SettingsStyle, + meta: &Metadata, screen_pos: Pos2, - edge_map: EdgeMap, ) -> Option> { - let pos_in_graph = (screen_pos.to_vec2() - meta.pan) / meta.zoom; - for ((start, end), edges) in edge_map { - let mut order = edges.len(); - for (idx_edge, e) in edges { - let pos_start = self.g.index(start).location().to_pos2(); - let pos_end = self.g.index(end).location().to_pos2(); - - let node_start = self.g.index(start); - let node_end = self.g.index(end); - - order -= 1; - - if start == end { - // edge is a loop (bezier curve) - let rad = node_start.screen_radius(meta, style) / meta.zoom; - let center = pos_start; - let center_horizon_angle = PI / 4.; - let y_intersect = center.y - rad * center_horizon_angle.sin(); - - let edge_start = - Pos2::new(center.x - rad * center_horizon_angle.cos(), y_intersect); - let edge_end = - Pos2::new(center.x + rad * center_horizon_angle.cos(), y_intersect); - - let loop_size = rad * (style.edge_looped_size + order as f32); - - let control_point1 = Pos2::new(center.x + loop_size, center.y - loop_size); - let control_point2 = Pos2::new(center.x - loop_size, center.y - loop_size); - - let shape = CubicBezierShape::from_points_stroke( - [edge_end, control_point1, control_point2, edge_start], - false, - Color32::default(), - Stroke::default(), - ); - if is_point_on_cubic_bezier_curve(pos_in_graph, shape, e.width(), meta.zoom) { - return Option::new(idx_edge); - } - - continue; - } - - if order == 0 { - // edge is a straight line between nodes - let distance = distance_segment_to_point( - pos_start.to_vec2(), - pos_end.to_vec2(), - pos_in_graph, - ); - if distance < e.width() { - return Option::new(idx_edge); - } - - continue; - } - - // multiple edges between nodes -> curved - let rad_start = node_start.screen_radius(meta, style) / meta.zoom; - let rad_end = node_end.screen_radius(meta, style) / meta.zoom; - - let vec = pos_end - pos_start; - let dist: f32 = vec.length(); - let dir = vec / dist; - - let start_node_radius_vec = Vec2::new(rad_start, rad_start) * dir; - let end_node_radius_vec = Vec2::new(rad_end, rad_end) * dir; - - let tip_end = pos_start + vec - end_node_radius_vec; - - let edge_start = pos_start + start_node_radius_vec; - - let dir_perpendicular = Vec2::new(-dir.y, dir.x); - let center_point = (edge_start + tip_end.to_vec2()).to_vec2() / 2.0; - let control_point = - (center_point + dir_perpendicular * e.curve_size() * order as f32).to_pos2(); - - let tip_vec = control_point - tip_end; - let tip_dir = tip_vec / tip_vec.length(); - let tip_size = e.tip_size(); - - let arrow_tip_dir_1 = rotate_vector(tip_dir, e.tip_angle()) * tip_size; - let arrow_tip_dir_2 = rotate_vector(tip_dir, -e.tip_angle()) * tip_size; - - let tip_start_1 = tip_end + arrow_tip_dir_1; - let tip_start_2 = tip_end + arrow_tip_dir_2; - - let edge_end_curved = point_between(tip_start_1, tip_start_2); - - let shape = QuadraticBezierShape::from_points_stroke( - [edge_start, control_point, edge_end_curved], - false, - Color32::default(), - Stroke::default(), - ); - if is_point_on_quadratic_bezier_curve(pos_in_graph, shape, e.width(), meta.zoom) { - return Option::new(idx_edge); - } + let pos_in_graph = meta.screen_to_canvas_pos(screen_pos); + for (idx, e) in self.edges_iter() { + if De::from(e.clone()).is_inside::(self, pos_in_graph) { + return Some(idx); } } None } - pub fn g(&mut self) -> &mut StableGraph, Edge, Ty, Ix> { + pub fn g(&mut self) -> &mut StableGraph, Edge, Ty, Ix> { &mut self.g } ///Provides iterator over all nodes and their indices. - pub fn nodes_iter(&'a self) -> impl Iterator, &Node)> { + pub fn nodes_iter(&self) -> impl Iterator, &Node)> { self.g.node_references() } /// Provides iterator over all edges and their indices. - pub fn edges_iter(&'a self) -> impl Iterator, &Edge)> { + pub fn edges_iter(&self) -> impl Iterator, &Edge)> { self.g.edge_references().map(|e| (e.id(), e.weight())) } - pub fn node(&self, i: NodeIndex) -> Option<&Node> { + pub fn node(&self, i: NodeIndex) -> Option<&Node> { self.g.node_weight(i) } - pub fn edge(&self, i: EdgeIndex) -> Option<&Edge> { + pub fn edge(&self, i: EdgeIndex) -> Option<&Edge> { self.g.edge_weight(i) } @@ -190,11 +88,11 @@ impl<'a, N: Clone, E: Clone + 'a, Ty: EdgeType, Ix: IndexType> Graph) -> Option<&mut Node> { + pub fn node_mut(&mut self, i: NodeIndex) -> Option<&mut Node> { self.g.node_weight_mut(i) } - pub fn edge_mut(&mut self, i: EdgeIndex) -> Option<&mut Edge> { + pub fn edge_mut(&mut self, i: EdgeIndex) -> Option<&mut Edge> { self.g.edge_weight_mut(i) } @@ -210,192 +108,7 @@ impl<'a, N: Clone, E: Clone + 'a, Ty: EdgeType, Ix: IndexType> Graph, dir: Direction, - ) -> impl Iterator, Ix>> { + ) -> impl Iterator, Ix>> { self.g.edges_directed(idx, dir) } } - -/// Returns the distance from line segment `a``b` to point `c`. -/// Adapted from https://stackoverflow.com/questions/1073336/circle-line-segment-collision-detection-algorithm -fn distance_segment_to_point(a: Vec2, b: Vec2, point: Vec2) -> f32 { - let ac = point - a; - let ab = b - a; - - let d = a + proj(ac, ab); - - let ad = d - a; - - let k = if ab.x.abs() > ab.y.abs() { - ad.x / ab.x - } else { - ad.y / ab.y - }; - - if k <= 0.0 { - return hypot2(point, a).sqrt(); - } else if k >= 1.0 { - return hypot2(point, b).sqrt(); - } - - hypot2(point, d).sqrt() -} - -/// Calculates the square of the Euclidean distance between vectors `a` and `b`. -fn hypot2(a: Vec2, b: Vec2) -> f32 { - (a - b).dot(a - b) -} - -/// Calculates the projection of vector `a` onto vector `b`. -fn proj(a: Vec2, b: Vec2) -> Vec2 { - let k = a.dot(b) / b.dot(b); - Vec2::new(k * b.x, k * b.y) -} - -fn is_point_on_cubic_bezier_curve( - point: Vec2, - curve: CubicBezierShape, - width: f32, - zoom: f32, -) -> bool { - is_point_on_bezier_curve(point, curve.flatten(Option::new(10.0 / zoom)), width) -} - -fn is_point_on_quadratic_bezier_curve( - point: Vec2, - curve: QuadraticBezierShape, - width: f32, - zoom: f32, -) -> bool { - is_point_on_bezier_curve(point, curve.flatten(Option::new(0.3 / zoom)), width) -} - -fn is_point_on_bezier_curve(point: Vec2, curve_points: Vec, width: f32) -> bool { - let mut previous_point = None; - for p in curve_points { - if let Some(pp) = previous_point { - let distance = distance_segment_to_point(p.to_vec2(), pp, point); - if distance < width { - return true; - } - } - previous_point = Some(p.to_vec2()); - } - false -} - -/// rotates vector by angle -fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 { - let cos = angle.cos(); - let sin = angle.sin(); - Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y) -} - -/// finds point exactly in the middle between 2 points -fn point_between(p1: Pos2, p2: Pos2) -> Pos2 { - let base = p1 - p2; - let base_len = base.length(); - let dir = base / base_len; - p1 - (base_len / 2.) * dir -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_distance_segment_to_point() { - let segment_1 = Vec2::new(2.0, 2.0); - let segment_2 = Vec2::new(2.0, 5.0); - let point = Vec2::new(4.0, 3.0); - assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 2.0); - } - - #[test] - fn test_distance_segment_to_point_on_segment() { - let segment_1 = Vec2::new(1.0, 2.0); - let segment_2 = Vec2::new(1.0, 5.0); - let point = Vec2::new(1.0, 3.0); - assert_eq!(distance_segment_to_point(segment_1, segment_2, point), 0.0); - } - - #[test] - fn test_hypot2() { - let a = Vec2::new(0.0, 1.0); - let b = Vec2::new(0.0, 5.0); - assert_eq!(hypot2(a, b), 16.0); - } - - #[test] - fn test_hypot2_no_distance() { - let a = Vec2::new(0.0, 1.0); - assert_eq!(hypot2(a, a), 0.0); - } - - #[test] - fn test_proj() { - let a = Vec2::new(5.0, 8.0); - let b = Vec2::new(10.0, 0.0); - let result = proj(a, b); - assert_eq!(result.x, 5.0); - assert_eq!(result.y, 0.0); - } - - #[test] - fn test_proj_orthogonal() { - let a = Vec2::new(5.0, 0.0); - let b = Vec2::new(0.0, 5.0); - let result = proj(a, b); - assert_eq!(result.x, 0.0); - assert_eq!(result.y, 0.0); - } - - #[test] - fn test_proj_same_vector() { - let a = Vec2::new(5.3, 4.9); - assert_eq!(proj(a,a), a); - } - - #[test] - fn test_is_point_on_cubic_bezier_curve() { - let edge_start = Pos2::new(-3.0, 0.0); - let edge_end = Pos2::new(3.0, 0.0); - let control_point1 = Pos2::new(-3.0, 3.0); - let control_point2 = Pos2::new(4.0, 2.0); - let curve = CubicBezierShape::from_points_stroke( - [edge_end, control_point1, control_point2, edge_start], - false, - Color32::default(), - Stroke::default(), - ); - - let zoom = 5.0; - let width = 1.0; - let p1 = Vec2::new(0.0, 2.0); - assert!(is_point_on_cubic_bezier_curve(p1, curve, width, zoom)); - - let p2 = Vec2::new(2.0, 1.0); - assert!(is_point_on_cubic_bezier_curve(p2, curve, width, zoom)); - } - - #[test] - fn test_is_point_on_quadratic_bezier_curve() { - let edge_start = Pos2::new(0.0, 0.0); - let edge_end = Pos2::new(20.0, 0.0); - let control_point = Pos2::new(10.0, 8.0); - let curve = QuadraticBezierShape::from_points_stroke( - [edge_start, control_point, edge_end], - false, - Color32::default(), - Stroke::default(), - ); - - let zoom = 5.0; - let width = 1.0; - let p1 = Vec2::new(10.0, 4.0); - assert!(is_point_on_quadratic_bezier_curve(p1, curve, width, zoom)); - - let p2 = Vec2::new(3.0, 2.0); - assert!(is_point_on_quadratic_bezier_curve(p2, curve, width, zoom)); - } - -} diff --git a/src/graph_view.rs b/src/graph_view.rs index 3339b702..9e7dbb18 100644 --- a/src/graph_view.rs +++ b/src/graph_view.rs @@ -1,25 +1,29 @@ +use std::marker::PhantomData; + #[cfg(feature = "events")] use crate::events::{ Event, PayloadEdgeClick, PayloadEdgeDeselect, PayloadEdgeSelect, PayloadNodeClick, PayloadNodeDeselect, PayloadNodeDoubleClick, PayloadNodeDragEnd, PayloadNodeDragStart, PayloadNodeMove, PayloadNodeSelect, PayloadPan, PayloadZoom, }; -use crate::graph::EdgeMap; use crate::{ computed::ComputedState, - draw::{Drawer, FnCustomEdgeDraw, FnCustomNodeDraw}, + draw::Drawer, metadata::Metadata, settings::SettingsNavigation, settings::{SettingsInteraction, SettingsStyle}, Graph, }; +use crate::{ + draw::{DefaultEdgeShape, DefaultNodeShape, DrawContext}, + DisplayEdge, DisplayNode, +}; #[cfg(feature = "events")] use crossbeam::channel::Sender; use egui::{Pos2, Rect, Response, Sense, Ui, Vec2, Widget}; -use petgraph::graph::EdgeIndex; -use petgraph::graph::IndexType; +use petgraph::{graph::EdgeIndex, stable_graph::DefaultIx}; +use petgraph::{graph::IndexType, Directed}; use petgraph::{stable_graph::NodeIndex, EdgeType}; -use std::collections::HashMap; /// Widget for visualizing and interacting with graphs. /// @@ -36,21 +40,42 @@ use std::collections::HashMap; /// When the user performs navigation actions (zoom & pan or fit to screen), they do not /// produce changes. This is because these actions are performed on the global coordinates and do not change any /// properties of the nodes or edges. -pub struct GraphView<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> { +pub struct GraphView< + 'a, + N, + E, + Ty = Directed, + Ix = DefaultIx, + Nd = DefaultNodeShape, + Ed = DefaultEdgeShape, +> where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Nd: DisplayNode, + Ed: DisplayEdge, +{ + g: &'a mut Graph, + settings_interaction: SettingsInteraction, settings_navigation: SettingsNavigation, settings_style: SettingsStyle, - g: &'a mut Graph, - - custom_edge_draw: Option>, - custom_node_draw: Option>, #[cfg(feature = "events")] events_publisher: Option<&'a Sender>, + + _marker: PhantomData<(Nd, Ed)>, } -impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> Widget - for &mut GraphView<'a, N, E, Ty, Ix> +impl<'a, N, E, Ty, Ix, Nd, Ed> Widget for &mut GraphView<'a, N, E, Ty, Ix, Nd, Ed> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Nd: DisplayNode, + Ed: DisplayEdge, { fn ui(self, ui: &mut Ui) -> Response { let (resp, p) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag()); @@ -61,16 +86,17 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> Widget self.handle_fit_to_screen(&resp, &mut meta, &computed); self.handle_navigation(ui, &resp, &mut meta, &computed); - self.handle_node_drag(&resp, &mut computed, &mut meta); + self.handle_node_drag::(&resp, &mut computed, &mut meta); self.handle_click(&resp, &mut meta, &computed); - Drawer::new( + Drawer::::new( p, - self.g, - &self.settings_style, - &meta, - self.custom_node_draw, - self.custom_edge_draw, + &DrawContext { + ctx: ui.ctx(), + g: self.g, + meta: &meta, + style: &self.settings_style, + }, ) .draw(); @@ -82,7 +108,15 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> Widget } } -impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty, Ix> { +impl<'a, N, E, Ty, Ix, Nd, Ed> GraphView<'a, N, E, Ty, Ix, Nd, Ed> +where + N: Clone, + E: Clone, + Ty: EdgeType, + Ix: IndexType, + Nd: DisplayNode, + Ed: DisplayEdge, +{ /// Creates a new `GraphView` widget with default navigation and interactions settings. /// To customize navigation and interactions use `with_interactions` and `with_navigations` methods. pub fn new(g: &'a mut Graph) -> Self { @@ -93,24 +127,11 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty settings_interaction: Default::default(), settings_navigation: Default::default(), - custom_node_draw: Default::default(), - custom_edge_draw: Default::default(), - #[cfg(feature = "events")] events_publisher: Default::default(), - } - } - - /// Sets a function that will be called instead of the default drawer for every node to draw custom shapes. - pub fn with_custom_node_draw(mut self, func: FnCustomNodeDraw) -> Self { - self.custom_node_draw = Some(func); - self - } - /// Sets a function that will be called instead of the default drawer for every pair of nodes connected with edges to draw custom shapes. - pub fn with_custom_edge_draw(mut self, func: FnCustomEdgeDraw) -> Self { - self.custom_edge_draw = Some(func); - self + _marker: PhantomData, + } } /// Makes widget interactive according to the provided settings. @@ -152,7 +173,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty let n = self.g.node_mut(*idx).unwrap(); n.set_computed(comp); - computed.comp_iter_bounds(n, &self.settings_style); + computed.comp_iter_bounds(n); }); self.g.edges_iter().for_each(|(idx, e)| { @@ -190,23 +211,12 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty return; } - let mut edge_map: EdgeMap = HashMap::new(); - - self.g.edges_iter().for_each(|(idx, e)| { - let (source, target) = self.g.edge_endpoints(idx).unwrap(); - // compute map with edges between 2 nodes - edge_map.entry((source, target)).or_default().push((idx, e)); - }); - - let found_edge = self.g.edge_by_screen_pos( - meta, - &self.settings_style, - resp.hover_pos().unwrap(), - edge_map, - ); - let found_node = - self.g - .node_by_screen_pos(meta, &self.settings_style, resp.hover_pos().unwrap()); + let found_edge = self + .g + .edge_by_screen_pos::(meta, resp.hover_pos().unwrap()); + let found_node = self + .g + .node_by_screen_pos::(meta, resp.hover_pos().unwrap()); if found_node.is_none() && found_edge.is_none() { // click on empty space let nodes_selectable = self.settings_interaction.node_selection_enabled @@ -307,7 +317,7 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty self.select_edge(idx); } - fn handle_node_drag( + fn handle_node_drag>( &mut self, resp: &Response, comp: &mut ComputedState, @@ -318,9 +328,9 @@ impl<'a, N: Clone, E: Clone, Ty: EdgeType, Ix: IndexType> GraphView<'a, N, E, Ty } if resp.drag_started() { - if let Some((idx, _)) = - self.g - .node_by_screen_pos(meta, &self.settings_style, resp.hover_pos().unwrap()) + if let Some((idx, _)) = self + .g + .node_by_screen_pos::(meta, resp.hover_pos().unwrap()) { self.set_drag_start(idx); } diff --git a/src/lib.rs b/src/lib.rs index 709869b5..1a6d7b26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,9 @@ mod settings; mod transform; pub use self::computed::ComputedNode; -pub use self::draw::{default_edges_draw, default_node_draw, FnCustomEdgeDraw, FnCustomNodeDraw}; +pub use self::draw::{ + DefaultEdgeShape, DefaultNodeShape, DisplayEdge, DisplayNode, DrawContext, Interactable, +}; pub use self::elements::{Edge, Node}; pub use self::graph::Graph; pub use self::graph_view::GraphView; diff --git a/src/metadata.rs b/src/metadata.rs index 7a431cdf..bf73e996 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -40,4 +40,16 @@ impl Metadata { data.insert_persisted(Id::null(), self); }); } + + pub fn canvas_to_screen_pos(&self, pos: Pos2) -> Pos2 { + (pos.to_vec2() * self.zoom + self.pan).to_pos2() + } + + pub fn canvas_to_screen_size(&self, size: f32) -> f32 { + size * self.zoom + } + + pub fn screen_to_canvas_pos(&self, pos: Pos2) -> Pos2 { + ((pos.to_vec2() - self.pan) / self.zoom).to_pos2() + } } diff --git a/src/settings.rs b/src/settings.rs index af17591a..7b31b63d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -141,23 +141,9 @@ impl SettingsNavigation { } /// `SettingsStyle` stores settings for the style of the graph. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct SettingsStyle { pub(crate) labels_always: bool, - pub(crate) edge_radius_weight: f32, - - /// Loop size for looped edges. - pub(crate) edge_looped_size: f32, -} - -impl Default for SettingsStyle { - fn default() -> Self { - Self { - edge_radius_weight: 1., - edge_looped_size: 3., - labels_always: Default::default(), - } - } } impl SettingsStyle { @@ -177,12 +163,4 @@ impl SettingsStyle { self.labels_always = always; self } - - /// For every edge connected to node its radius is getting bigger by this value. - /// - /// Default: `1.` - pub fn with_edge_radius_weight(mut self, weight: f32) -> Self { - self.edge_radius_weight = weight; - self - } } diff --git a/src/transform.rs b/src/transform.rs index a26bb375..b79bfa7a 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,5 +1,5 @@ use crate::{Edge, Graph, Node}; -use egui::Vec2; +use egui::Pos2; use petgraph::{ graph::IndexType, stable_graph::{EdgeIndex, NodeIndex, StableGraph}, @@ -11,6 +11,9 @@ use std::collections::HashMap; pub const DEFAULT_SPAWN_SIZE: f32 = 250.; +pub type EdgeTransform = fn(EdgeIndex, &E, usize) -> Edge; +pub type NodeTransform = fn(NodeIndex, &N) -> Node; + /// Helper function which adds user's node to the [`super::Graph`] instance. /// /// If graph is not empty it picks any node position and adds new node in the vicinity of it. @@ -27,7 +30,7 @@ pub fn add_node( pub fn add_node_custom( g: &mut Graph, n: &N, - node_transform: impl Fn(NodeIndex, &N) -> Node, + node_transform: NodeTransform, ) -> NodeIndex { g.g.add_node(node_transform( NodeIndex::::new(g.g.node_count() + 1), @@ -51,12 +54,13 @@ pub fn add_edge_custom( start: NodeIndex, end: NodeIndex, e: &E, - edge_transform: impl Fn(EdgeIndex, &E) -> Edge, + edge_transform: EdgeTransform, ) -> EdgeIndex { + let order = g.g.edges_connecting(start, end).count(); g.g.add_edge( start, end, - edge_transform(EdgeIndex::::new(g.g.edge_count() + 1), e), + edge_transform(EdgeIndex::::new(g.g.edge_count() + 1), e, order), ) } @@ -78,7 +82,7 @@ pub fn add_edge_custom( /// ``` /// use petgraph::stable_graph::StableGraph; /// use egui_graphs::to_graph; -/// use egui::Vec2; +/// use egui::Pos2; /// /// let mut user_graph: StableGraph<&str, &str> = StableGraph::new(); /// let node1 = user_graph.add_node("A"); @@ -93,18 +97,18 @@ pub fn add_edge_custom( /// let mut input_indices = input_graph.g.node_indices(); /// let input_node_1 = input_indices.next().unwrap(); /// let input_node_2 = input_indices.next().unwrap(); -/// assert_eq!(*input_graph.g.node_weight(input_node_1).unwrap().data().clone().unwrap(), "A"); -/// assert_eq!(*input_graph.g.node_weight(input_node_2).unwrap().data().clone().unwrap(), "B"); +/// assert_eq!(*input_graph.g.node_weight(input_node_1).unwrap().payload().clone().unwrap(), "A"); +/// assert_eq!(*input_graph.g.node_weight(input_node_2).unwrap().payload().clone().unwrap(), "B"); /// -/// assert_eq!(*input_graph.g.edge_weight(input_graph.g.edge_indices().next().unwrap()).unwrap().data().clone().unwrap(), "edge1"); +/// assert_eq!(*input_graph.g.edge_weight(input_graph.g.edge_indices().next().unwrap()).unwrap().payload().clone().unwrap(), "edge1"); /// /// assert_eq!(*input_graph.g.node_weight(input_node_1).unwrap().label().clone(), input_node_1.index().to_string()); /// assert_eq!(*input_graph.g.node_weight(input_node_2).unwrap().label().clone(), input_node_2.index().to_string()); /// /// let loc_1 = input_graph.g.node_weight(input_node_1).unwrap().location(); /// let loc_2 = input_graph.g.node_weight(input_node_2).unwrap().location(); -/// assert!(loc_1 != Vec2::ZERO); -/// assert!(loc_2 != Vec2::ZERO); +/// assert!(loc_1 != Pos2::ZERO); +/// assert!(loc_2 != Pos2::ZERO); /// ``` pub fn to_graph( g: &StableGraph, @@ -115,35 +119,46 @@ pub fn to_graph( /// The same as [`to_graph`], but allows to define custom transformation procedures for nodes and edges. pub fn to_graph_custom( g: &StableGraph, - node_transform: impl Fn(NodeIndex, &N) -> Node, - edge_transform: impl Fn(EdgeIndex, &E) -> Edge, + node_transform: NodeTransform, + edge_transform: EdgeTransform, ) -> Graph { transform(g, node_transform, edge_transform) } /// Default node transform function. Keeps original data and creates a new node with a random location and /// label equal to the index of the node in the graph. -pub fn default_node_transform(idx: NodeIndex, data: &N) -> Node { +pub fn default_node_transform( + idx: NodeIndex, + payload: &N, +) -> Node { + let mut n = Node::new(payload.clone()).with_label(idx.index().to_string()); let loc = random_location(DEFAULT_SPAWN_SIZE); - Node::new(loc, data.clone()).with_label(idx.index().to_string()) + n.bind(idx, loc); + n } /// Default edge transform function. Keeps original data and creates a new edge. -pub fn default_edge_transform(_: EdgeIndex, data: &E) -> Edge { - Edge::new(data.clone()) +pub fn default_edge_transform( + idx: EdgeIndex, + payload: &E, + order: usize, +) -> Edge { + let mut e = Edge::new(payload.clone()); + e.bind(idx, order); + e } -fn random_location(size: f32) -> Vec2 { +fn random_location(size: f32) -> Pos2 { let mut rng = rand::thread_rng(); - Vec2::new(rng.gen_range(0. ..size), rng.gen_range(0. ..size)) + Pos2::new(rng.gen_range(0. ..size), rng.gen_range(0. ..size)) } fn transform( g: &StableGraph, - node_transform: impl Fn(NodeIndex, &N) -> Node, - edge_transform: impl Fn(EdgeIndex, &E) -> Edge, + node_transform: NodeTransform, + edge_transform: EdgeTransform, ) -> Graph { - let mut input_g = StableGraph::, Edge, Ty, Ix>::default(); + let mut input_g = StableGraph::, Edge, Ty, Ix>::default(); let input_by_user = g .node_references() @@ -160,10 +175,13 @@ fn transform( let input_source_n = *input_by_user.get(&user_source_n_idx).unwrap(); let input_target_n = *input_by_user.get(&user_target_n_idx).unwrap(); + let order = input_g + .edges_connecting(input_source_n, input_target_n) + .count(); input_g.add_edge( input_source_n, input_target_n, - edge_transform(user_e_idx, user_e), + edge_transform(user_e_idx, user_e, order), ); }); @@ -193,7 +211,7 @@ mod tests { let user_n = user_g.node_weight(user_idx).unwrap(); let input_n = input_g.g.node_weight(input_idx).unwrap(); - assert_eq!(*input_n.data().unwrap(), *user_n); + assert_eq!(*input_n.payload().unwrap(), *user_n); assert!(input_n.location().x >= 0.0 && input_n.location().x <= DEFAULT_SPAWN_SIZE); assert!(input_n.location().y >= 0.0 && input_n.location().y <= DEFAULT_SPAWN_SIZE); @@ -222,7 +240,7 @@ mod tests { let user_n = user_g.node_weight(user_idx).unwrap(); let input_n = input_g.g.node_weight(input_idx).unwrap(); - assert_eq!(*input_n.data().unwrap(), *user_n); + assert_eq!(*input_n.payload().unwrap(), *user_n); assert!(input_n.location().x >= 0.0 && input_n.location().x <= DEFAULT_SPAWN_SIZE); assert!(input_n.location().y >= 0.0 && input_n.location().y <= DEFAULT_SPAWN_SIZE);