Add initial force-directed graph script
This commit is contained in:
parent
325a85eb39
commit
420fc2089a
|
@ -4,6 +4,7 @@ members = [
|
|||
"apps/music-player",
|
||||
"apps/sandbox",
|
||||
"crates/script",
|
||||
"scripts/force-directed-graph",
|
||||
"scripts/music-player",
|
||||
"scripts/sao-ui",
|
||||
]
|
||||
|
|
|
@ -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" }
|
|
@ -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) {}
|
||||
}
|
Loading…
Reference in New Issue