Add initial force-directed graph script

This commit is contained in:
mars 2022-11-03 17:54:03 -06:00
parent 325a85eb39
commit 420fc2089a
3 changed files with 398 additions and 0 deletions

View File

@ -4,6 +4,7 @@ members = [
"apps/music-player",
"apps/sandbox",
"crates/script",
"scripts/force-directed-graph",
"scripts/music-player",
"scripts/sao-ui",
]

View File

@ -0,0 +1,10 @@
[package]
name = "canary-force-directed-graph-script"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
canary-script = { path = "../../crates/script" }

View File

@ -0,0 +1,387 @@
use canary_script::api::{BindPanel, DrawContext, Font, Message, Panel, PanelImpl, TextLayout};
use canary_script::{export_abi, Color, CursorEventKind, MeshVertex, Vec2};
static LABEL_FONT: &str = "Liberation Sans";
pub struct Body {
pub position: Vec2,
pub velocity: Vec2,
pub acceleration: Vec2,
pub mass: f32,
pub drag: f32,
pub fixed: bool,
}
impl Default for Body {
fn default() -> Self {
Self {
position: Vec2::ZERO,
velocity: Vec2::ZERO,
acceleration: Vec2::ZERO,
drag: 0.8,
mass: 40.0,
fixed: false,
}
}
}
impl Body {
pub fn apply_force(&mut self, force: Vec2) {
self.acceleration += force / self.mass; // F = ma
}
pub fn update(&mut self, dt: f32) {
self.velocity += self.acceleration * dt;
self.velocity *= self.drag.powf(dt);
self.acceleration = Vec2::ZERO;
if !self.fixed {
self.position += self.velocity * dt;
}
}
}
pub struct NodeInfo {
pub label: TextLayout,
pub label_offset: Vec2,
pub text_size: f32,
pub hidden: bool,
}
impl NodeInfo {
pub fn new(label: &str) -> Self {
let text_size = 5.0;
let label = TextLayout::new(&Font::new(LABEL_FONT), &label);
let bounds = label.get_bounds();
let label_offset = Vec2::new(-bounds.width() / 2.0, text_size / 2.0);
Self {
label,
label_offset,
text_size,
hidden: false,
}
}
}
pub struct Node {
pub body: Body,
pub info: NodeInfo,
}
#[derive(Debug, Clone)]
pub struct MenuNode {
pub label: String,
pub children: Vec<MenuNode>,
}
impl MenuNode {
pub fn make_test() -> Self {
Self {
label: "Menu".to_string(),
children: vec![
Self {
label: "File".to_string(),
children: vec![
Self {
label: "Open...".to_string(),
children: vec![],
},
Self {
label: "Save as...".to_string(),
children: vec![],
},
Self {
label: "Save".to_string(),
children: vec![],
},
Self {
label: "Quit".to_string(),
children: vec![],
},
],
},
Self {
label: "Edit".to_string(),
children: vec![
Self {
label: "Undo".to_string(),
children: vec![],
},
Self {
label: "Redo".to_string(),
children: vec![],
},
],
},
Self {
label: "View".to_string(),
children: vec![
Self {
label: "Zoom in".to_string(),
children: vec![],
},
Self {
label: "Zoom out".to_string(),
children: vec![],
},
Self {
label: "Reset zoom".to_string(),
children: vec![],
},
Self {
label: "Fullscreen".to_string(),
children: vec![],
},
],
},
Self {
label: "Help".to_string(),
children: vec![Self {
label: "About".to_string(),
children: vec![],
}],
},
],
}
}
pub fn make_force_graph(&self) -> ForceGraph {
let mut graph = ForceGraph::default();
let root = Node {
body: Body {
fixed: true,
..Default::default()
},
info: NodeInfo::new(&self.label),
};
let parent = graph.nodes.len();
graph.nodes.push(root);
self.build_force_graph(&mut graph, parent);
graph
}
pub fn build_force_graph(&self, graph: &mut ForceGraph, parent: usize) {
let mut siblings = Vec::new();
for child in self.children.iter() {
let id = graph.nodes.len();
graph.nodes.push(Node {
body: Body {
position: Vec2::from_angle(id as f32),
..Default::default()
},
info: NodeInfo::new(&child.label),
});
graph.springs.push(Spring {
from: parent,
to: id,
length: 30.0,
stiffness: 0.5,
margin: 10.0,
});
for sibling in siblings.iter() {
graph.springs.push(Spring {
from: *sibling,
to: id,
length: 80.0,
stiffness: 0.5,
margin: 10.0,
});
}
siblings.push(id);
graph.connections.push(Connection {
from: parent,
to: id,
width: 1.0,
color: Color::WHITE,
});
child.build_force_graph(graph, id);
}
}
}
#[derive(Clone)]
pub struct Spring {
pub from: usize,
pub to: usize,
pub length: f32,
pub stiffness: f32,
pub margin: f32,
}
impl Spring {
pub fn hookes_law(&self, delta: Vec2) -> Vec2 {
const MAX_DISPLACE: f32 = 20.0;
let distance = delta.length();
let mut displacement = distance - self.length;
/*if displacement.abs() < self.margin {
return Vec2::ZERO;
} else if displacement.is_sign_positive() {
displacement -= self.margin;
} else {
displacement += self.margin;
}*/
let displacement = displacement.clamp(-MAX_DISPLACE, MAX_DISPLACE);
let force = displacement * self.stiffness * delta.normalize_or_zero();
force
}
}
#[derive(Clone)]
pub struct Connection {
pub from: usize,
pub to: usize,
pub width: f32,
pub color: Color,
}
#[derive(Default)]
pub struct ForceGraph {
pub nodes: Vec<Node>,
pub springs: Vec<Spring>,
pub connections: Vec<Connection>,
}
impl ForceGraph {
pub fn update(&mut self, dt: f32) {
self.apply_springs();
self.apply_repulsion();
for node in self.nodes.iter_mut() {
node.body.update(dt);
}
}
pub fn apply_springs(&mut self) {
for spring in self.springs.iter() {
let from = &self.nodes[spring.from].body;
let to = &self.nodes[spring.to].body;
let delta = to.position - from.position;
let force = spring.hookes_law(delta);
self.nodes[spring.from].body.apply_force(force);
self.nodes[spring.to].body.apply_force(-force);
}
}
pub fn apply_repulsion(&mut self) {
for i in 1..self.nodes.len() {
for j in i..self.nodes.len() {
const REPULSION_CONSTANT: f32 = 50.0;
let from = self.nodes[i - 1].body.position;
let to = self.nodes[j].body.position;
let delta = to - from;
let proximity = delta.length().max(0.1);
let force = -(REPULSION_CONSTANT / (proximity * proximity));
let force = delta * force;
self.nodes[i - 1].body.apply_force(force);
self.nodes[j].body.apply_force(-force);
}
}
}
pub fn draw(&self, ctx: &DrawContext) {
let radius = 5.0;
let thickness = 1.0;
let color = Color::WHITE;
let label_color = Color::WHITE;
for node in self.nodes.iter() {
ctx.draw_ring(node.body.position, radius, thickness, color);
let offset = node.body.position + node.info.label_offset;
ctx.draw_text_layout(&node.info.label, offset, node.info.text_size, label_color);
}
for connection in self.connections.iter() {
let from = self.nodes[connection.from].body.position;
let to = self.nodes[connection.to].body.position;
let delta = to - from;
if delta.length() < radius * 2.0 {
continue;
}
let delta_norm = delta.normalize();
let from = from + delta_norm * radius;
let to = to - delta_norm * radius;
let delta_cross = Vec2::new(delta_norm.y, -delta_norm.x);
let delta_side = delta_cross * connection.width / 2.0;
let vertices = [
MeshVertex {
position: from + delta_side,
color,
},
MeshVertex {
position: from - delta_side,
color,
},
MeshVertex {
position: to + delta_side,
color,
},
MeshVertex {
position: to - delta_side,
color,
},
];
let indices = [0, 1, 2, 1, 2, 3];
ctx.draw_indexed(&vertices, &indices);
}
}
}
export_abi!(ForceDirectedGraphPanel);
pub struct ForceDirectedGraphPanel {
panel: Panel,
graph: ForceGraph,
size: Vec2,
}
impl BindPanel for ForceDirectedGraphPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let menu = MenuNode::make_test();
let graph = menu.make_force_graph();
let panel = Self {
panel,
graph,
size: Vec2::splat(100.0),
};
Box::new(panel)
}
}
impl PanelImpl for ForceDirectedGraphPanel {
fn update(&mut self, dt: f32) {
self.graph.update(dt);
}
fn draw(&mut self) {
let ctx = DrawContext::new(self.panel).with_offset(self.size / 2.0);
self.graph.draw(&ctx);
}
fn on_resize(&mut self, new_size: Vec2) {
self.size = new_size;
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
fn on_message(&mut self, msg: Message) {}
}