Add force directed flow chart

This commit is contained in:
Michael Mikovsky
2025-11-27 16:52:38 -07:00
parent 3d9332059a
commit dca08b9e97
11 changed files with 521 additions and 118 deletions
+6 -6
View File
@@ -31,17 +31,17 @@ impl Default for TemplateApp {
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
} else {
Default::default()
}
// if let Some(storage) = cc.storage {
// eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
// } else {
Default::default()
// }
}
}
+16
View File
@@ -100,6 +100,7 @@ impl Config {
.column(Column::remainder())
.column(Column::remainder())
.column(Column::remainder())
.column(Column::auto().resizable(false))
.min_scrolled_height(0.0)
.sense(egui::Sense::click());
@@ -117,6 +118,7 @@ impl Config {
header.col(|ui| {
ui.strong("Runtimes");
});
header.col(|_| {});
})
.body(|mut body| {
for i in 0..self.current_payloads.len() {
@@ -138,6 +140,20 @@ impl Config {
row.col(|ui| {
ui.label("A");
});
row.col(|ui| {
if ui.button("Edit").clicked() {
self.state = ConfigState::EditConfig(
i,
self.current_payloads[i].clone(),
);
}
// if ui.button("Delete").clicked() {
// self.state = ConfigState::EditConfig(
// i,
// self.current_payloads[i].clone(),
// );
// }
});
if row.response().clicked() {
self.state = ConfigState::EditConfig(
-112
View File
@@ -1,112 +0,0 @@
use egui::Ui;
use egui::{Color32, Painter, Pos2, Rect, Stroke, UiBuilder, Vec2};
const TARGET_LINE_GAP: f32 = 80.;
const BG_STROKE: Stroke = Stroke {
width: 0.3,
color: Color32::GRAY,
};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct FlowChart {
// frame: Frame,
container: DraggableContainer,
}
impl FlowChart {
pub fn new() -> Self {
Self {
container: DraggableContainer::new(
Pos2 { x: 100., y: 100. },
Vec2 { x: 100., y: 100. },
),
}
}
fn paint_bg(&self, rect: &Rect, painter: &Painter) {
let h_count = (rect.width() / TARGET_LINE_GAP).round() as usize;
let h_spacing = rect.width() / h_count as f32;
for n in 0..h_count {
painter.vline(rect.min.x + n as f32 * h_spacing, rect.y_range(), BG_STROKE);
}
let v_count = (rect.height() / TARGET_LINE_GAP).round() as usize;
let v_spacing = rect.height() / v_count as f32;
for n in 0..v_count {
painter.hline(rect.x_range(), rect.min.y + n as f32 * v_spacing, BG_STROKE);
}
}
pub fn paint(&mut self, ui: &mut Ui) {
self.paint_bg(&ui.clip_rect(), ui.painter());
self.container.show(ui, |ui, rect| {
ui.painter().rect(
// ui.top
*rect,
0.,
Color32::PURPLE,
BG_STROKE,
egui::StrokeKind::Outside,
);
ui.label("Tests");
let _ = ui.button("Test");
})
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct DraggableContainer {
pub pos: egui::Pos2,
pub size: egui::Vec2,
is_dragging: bool,
drag_offset: egui::Vec2,
}
impl DraggableContainer {
pub fn new(pos: egui::Pos2, size: egui::Vec2) -> Self {
Self {
pos,
size,
is_dragging: false,
drag_offset: egui::Vec2::ZERO,
}
}
pub fn show<R>(
&mut self,
ui: &mut egui::Ui,
add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
) -> R {
let rect = egui::Rect::from_min_size(self.pos, self.size);
// Handle dragging logic
let response = ui.interact(rect, ui.id().with("drag_area"), egui::Sense::drag());
if response.drag_started() {
self.is_dragging = true;
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
self.drag_offset = self.pos - pointer_pos;
}
}
if response.dragged() && self.is_dragging {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
self.pos = pointer_pos + self.drag_offset;
}
}
if response.drag_stopped() {
self.is_dragging = false;
}
// Create a child UI at the specified position
// let mut child_ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::LEFT), None);
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
// Add contents
add_contents(&mut child_ui, &rect)
}
}
+117
View File
@@ -0,0 +1,117 @@
use egui::{Pos2, Rect, UiBuilder, Vec2};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct DraggableContainer {
pub pos: egui::Vec2, // Offset from center of clip_rect
pub size: egui::Vec2,
is_dragging: bool,
drag_offset: egui::Vec2,
drag_id: String,
pub vel: egui::Vec2,
}
impl DraggableContainer {
// pub fn new(center_offset: egui::Vec2, size: egui::Vec2, id: usize) -> Self {
// Self {
// pos: center_offset,
// size,
// is_dragging: false,
// drag_offset: egui::Vec2::ZERO,
// drag_id: format!("flowchart_drag_area{}", id),
// vel: Vec2::ZERO,
// }
// }
pub fn new_zero(id: usize) -> Self {
Self {
pos: Vec2::ZERO,
size: Vec2 { x: 100., y: 100. },
is_dragging: false,
drag_offset: egui::Vec2::ZERO,
drag_id: format!("flowchart_drag_area{}", id),
vel: Vec2::ZERO,
}
}
// pub fn show<R>(
// &mut self,
// ui: &mut egui::Ui,
// add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
// ) -> R {
// let rect = egui::Rect::from_min_size(self.pos, self.size);
// // Handle dragging logic
// let response = ui.interact(rect, ui.id().with(&self.drag_id), egui::Sense::drag());
// if response.drag_started() {
// self.is_dragging = true;
// if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
// self.drag_offset = self.pos - pointer_pos;
// }
// }
// if response.dragged() && self.is_dragging {
// if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
// self.pos = pointer_pos + self.drag_offset;
// }
// }
// if response.drag_stopped() {
// self.is_dragging = false;
// }
// // Create a child UI at the specified position
// // let mut child_ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::LEFT), None);
// let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
// // Add contents
// add_contents(&mut child_ui, &rect)
// }
pub fn get_pos(&self, center: &Pos2) -> Pos2 {
center.clone() + self.pos
}
pub fn show<R>(
&mut self,
ui: &mut egui::Ui,
add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
) -> R {
// Calculate center of the clip rect
let clip_center = ui.clip_rect().center();
// Calculate actual position from center offset
let center_pos = clip_center + self.pos;
let pos = center_pos - self.size / 2.0; // Top-left corner from center
let rect = egui::Rect::from_min_size(pos, self.size);
// Handle dragging logic
let response = ui.interact(rect, ui.id().with(&self.drag_id), egui::Sense::drag());
if response.drag_started() {
self.is_dragging = true;
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
self.drag_offset = center_pos - pointer_pos;
}
}
if response.dragged() && self.is_dragging {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
let new_center = pointer_pos + self.drag_offset;
self.pos = new_center - clip_center;
}
}
if response.drag_stopped() {
self.is_dragging = false;
}
// Create a child UI at the specified position
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
// Add contents
add_contents(&mut child_ui, &rect)
}
}
+152
View File
@@ -0,0 +1,152 @@
use egui::Shape;
use egui::Ui;
use egui::{Color32, Painter, Pos2, Rect};
use crate::flowchart::CONNECTION_STROKE;
use crate::flowchart::GROUP_BORDER_MARGIN;
use crate::flowchart::container::DraggableContainer;
use crate::flowchart::group::convex_hull;
use crate::flowchart::{BG_STROKE, TARGET_LINE_GAP};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct FlowChart {
pub containers: Vec<DraggableContainer>,
pub connections: Vec<(usize, usize)>,
pub groups: Vec<Vec<usize>>,
}
impl FlowChart {
pub fn new() -> Self {
let mut this = Self {
containers: vec![
DraggableContainer::new_zero(0),
DraggableContainer::new_zero(1),
DraggableContainer::new_zero(2),
DraggableContainer::new_zero(3),
DraggableContainer::new_zero(4),
DraggableContainer::new_zero(5),
DraggableContainer::new_zero(6),
DraggableContainer::new_zero(7),
],
connections: vec![(0, 1), (1, 2), (1, 3), (1, 4), (3, 5), (3, 6), (3, 7)],
groups: vec![],
};
this.arrange_circle();
this
}
fn paint_bg(&self, rect: &Rect, painter: &Painter) {
let h_count = (rect.width() / TARGET_LINE_GAP).round() as usize;
let h_spacing = rect.width() / h_count as f32;
for n in 0..h_count {
painter.vline(rect.min.x + n as f32 * h_spacing, rect.y_range(), BG_STROKE);
}
let v_count = (rect.height() / TARGET_LINE_GAP).round() as usize;
let v_spacing = rect.height() / v_count as f32;
for n in 0..v_count {
painter.hline(rect.x_range(), rect.min.y + n as f32 * v_spacing, BG_STROKE);
}
}
fn paint_groups(&self, ui: &mut Ui) {
let center = ui.clip_rect().center();
for group in &self.groups {
let mut points = Vec::new();
for n in group {
let container = &self.containers[*n];
let pos = container.get_pos(&center);
let size = container.size;
points.append(&mut vec![
Pos2 {
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
y: pos.y - size.x / 2. - GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
y: pos.y - size.y / 2. - GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
},
Pos2 {
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
},
]);
}
let points = convex_hull(&points);
ui.painter().add(Shape::convex_polygon(
points,
Color32::DEBUG_COLOR,
BG_STROKE,
));
}
}
pub fn paint(&mut self, ui: &mut Ui) {
self.paint_bg(&ui.clip_rect(), ui.painter());
self.paint_groups(ui);
let center = ui.clip_rect().center();
for (a, b) in &self.connections {
ui.painter().line_segment(
[
self.containers[*a].get_pos(&center),
self.containers[*b].get_pos(&center),
],
CONNECTION_STROKE,
);
// let start = self.containers[m.clone()];
// let end = self.containers[n.clone()];
}
for container in &mut self.containers {
container.show(ui, |ui, rect| {
ui.painter().rect(
// ui.top
*rect,
0.,
Color32::PURPLE,
BG_STROKE,
egui::StrokeKind::Outside,
);
// ui.label("Tests");
// let _ = ui.button("Test");
});
}
if ui.button("Arrange").clicked() {
// let positions: Vec<Vec2> = (0..num_nodes)
// .map(|i| {
// let angle = (i as f32) * 2.0 * std::f32::consts::PI / (num_nodes as f32);
// Vec2::new(angle.cos() * 100.0, angle.sin() * 100.0)
// })
// .collect();
// let node_count = self.containers.len() as f32;
// for (i, m) in self.containers.iter_mut().enumerate() {
// let ang = -(i as f32 / node_count) * PI * 2.;
// m.pos = Vec2 {
// x: 1000. * ang.sin(),
// y: 1000. * ang.cos(),
// };
// m.vel = Vec2::ZERO;
// }
for _ in 0..1_000 {
self.force(0.1);
}
}
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::f32::consts::TAU;
use egui::Vec2;
use crate::flowchart::{
ATTRACTION_STRENGTH, CENTER_ATTRACTION_STRENGTH, DAMPING, FlowChart, GROUP_ATTRACTION_STRENGTH,
REPULSION_STRENGTH, REST_LENGTH,
};
pub fn normalize(v: &Vec2) -> Vec2 {
let len = v.length();
if len > 0.0 {
Vec2 {
x: v.x / len,
y: v.y / len,
}
} else {
Vec2 { x: 0.0, y: 0.0 }
}
}
impl FlowChart {
pub fn force(&mut self, delta_time: f32) {
let num_nodes = self.containers.len();
let mut forces = vec![Vec2::new(0.0, 0.0); num_nodes];
// Calculate repulsive forces between all nodes
for i in 0..num_nodes {
for j in (i + 1)..num_nodes {
let diff = self.containers[i].pos - self.containers[j].pos;
let dist = diff.length().max(0.1); // Prevent division by zero
let force = normalize(&diff) * (REPULSION_STRENGTH / (dist * dist));
forces[i] = forces[i] + force;
forces[j] = forces[j] + (force * -1.0);
}
}
// Calculate attractive forces along connections
for &(i, j) in &self.connections {
let diff = self.containers[j].pos - self.containers[i].pos;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * ATTRACTION_STRENGTH);
forces[i] = forces[i] + force;
forces[j] = forces[j] + (force * -1.0);
}
// Apply force to center
for i in 0..num_nodes {
let diff = self.containers[i].pos;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * CENTER_ATTRACTION_STRENGTH);
forces[i] = forces[i] + force * -1.;
}
let group_avg = &self
.groups
.iter()
.map(|group| {
let mut sum = Vec2::ZERO;
for n in group {
sum += self.containers[*n].pos;
}
sum / group.len() as f32
})
.collect::<Vec<Vec2>>();
for (group, group_avg) in self.groups.iter().zip(group_avg) {
for i in 0..num_nodes {
let diff = self.containers[i].pos - *group_avg;
let dist = diff.length();
let displacement = dist - REST_LENGTH;
let force = normalize(&diff) * (displacement * GROUP_ATTRACTION_STRENGTH);
if group.contains(&i) {
forces[i] = forces[i] + force * -1.;
} else {
forces[i] = forces[i] + force;
}
}
}
// Update velocities and positions
for i in 0..num_nodes {
let c = &mut self.containers[i];
c.vel = (c.vel + forces[i] * delta_time) * DAMPING;
c.pos += c.vel * delta_time;
}
}
pub fn arrange_circle(&mut self) {
let node_count = self.containers.len() as f32;
for (i, m) in self.containers.iter_mut().enumerate() {
let ang = -(i as f32 / node_count) * TAU;
m.pos = Vec2 {
x: 300. * ang.sin(),
y: 300. * ang.cos(),
};
m.vel = Vec2::ZERO;
}
}
// pub fn get_positions(&self) -> &[Vec2] {
// &self.positions
// }
// pub fn get_connections(&self) -> &[(usize, usize)] {
// &self.connections
// }
}
// fn main() {
// // Example usage: Create a simple triangle graph
// let connections = vec![(0, 1), (1, 2), (2, 0)];
// let mut graph = ForceDirectedGraph::new(
// 3,
// connections,
// 1000.0, // repulsion_strength
// 0.1, // attraction_strength
// 50.0, // rest_length
// 0.9, // damping
// );
// // Simulate 100 frames at 60 FPS
// let delta_time = 1.0 / 60.0;
// for frame in 0..100 {
// graph.update(delta_time);
// if frame % 20 == 0 {
// println!("Frame {}: ", frame);
// for (i, pos) in graph.get_positions().iter().enumerate() {
// println!(" Node {}: ({:.2}, {:.2})", i, pos.x, pos.y);
// }
// }
// }
// }
+59
View File
@@ -0,0 +1,59 @@
use egui::Pos2;
/// Calculate the convex hull of a set of points using Graham scan
pub fn convex_hull(points: &[Pos2]) -> Vec<Pos2> {
if points.len() < 3 {
return points.to_vec();
}
let mut pts = points.to_vec();
// Find the point with lowest y-coordinate (and leftmost if tie)
let start_idx = pts
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.y.partial_cmp(&b.y)
.unwrap()
.then(a.x.partial_cmp(&b.x).unwrap())
})
.unwrap()
.0;
pts.swap(0, start_idx);
let start = pts[0];
// Sort points by polar angle with respect to start point
pts[1..].sort_by(|a, b| {
let angle_a = polar_angle_to(&start, a);
let angle_b = polar_angle_to(&start, b);
angle_a.partial_cmp(&angle_b).unwrap()
});
// Build convex hull
let mut hull = Vec::new();
hull.push(pts[0]);
hull.push(pts[1]);
for i in 2..pts.len() {
while hull.len() > 1
&& cross_product(&hull[hull.len() - 2], &hull[hull.len() - 1], &pts[i]) <= 0.0
{
hull.pop();
}
hull.push(pts[i]);
}
hull
}
/// Calculate cross product of vectors (self->p2) and (self->p3)
/// Positive if counter-clockwise, negative if clockwise, zero if collinear
fn cross_product(p1: &Pos2, p2: &Pos2, p3: &Pos2) -> f32 {
(p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
}
/// Calculate polar angle from self to other point
fn polar_angle_to(a: &Pos2, b: &Pos2) -> f32 {
(b.y - a.y).atan2(b.x - a.x)
}
+28
View File
@@ -0,0 +1,28 @@
mod container;
mod flowchart;
mod force;
mod group;
use egui::{Color32, Stroke};
pub use flowchart::FlowChart;
const TARGET_LINE_GAP: f32 = 80.;
const BG_STROKE: Stroke = Stroke {
width: 0.3,
color: Color32::GRAY,
};
const CONNECTION_STROKE: Stroke = Stroke {
width: 3.,
color: Color32::WHITE,
};
const GROUP_BORDER_MARGIN: f32 = 20.;
static REPULSION_STRENGTH: f32 = 100000.0; // repulsion_strength
static ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
static CENTER_ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
static GROUP_ATTRACTION_STRENGTH: f32 = 0.001; // attraction_strength
static REST_LENGTH: f32 = 50.0; // rest_length
static DAMPING: f32 = 0.9; // damping
+1
View File
@@ -1,5 +1,6 @@
#![warn(clippy::all, rust_2018_idioms)]
#![macro_use]
#[allow(unused_extern_crates)]
extern crate log;
mod app;