Compare commits
13 Commits
main
...
force-dire
Author | SHA1 | Date |
---|---|---|
mars | 14f432543a | |
mars | 33bf08d4c1 | |
mars | 351a85f484 | |
mars | 69355204d7 | |
mars | ca3877f96c | |
mars | be816127fb | |
mars | 3a04fe95b6 | |
mars | a530600e54 | |
mars | eac404e976 | |
mars | dfb160ffef | |
mars | 6f0be98a11 | |
mars | 647b6ec59a | |
mars | 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,566 @@
|
|||
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 friction: f32,
|
||||
pub fixed: bool,
|
||||
}
|
||||
|
||||
impl Default for Body {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Vec2::ZERO,
|
||||
velocity: Vec2::ZERO,
|
||||
acceleration: Vec2::ZERO,
|
||||
friction: 0.001,
|
||||
mass: 1.0,
|
||||
fixed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
pub fn apply_force(&mut self, force: Vec2) {
|
||||
self.acceleration += force / self.mass; // F = ma
|
||||
}
|
||||
|
||||
pub fn apply_velocity(&mut self, vel: Vec2) {
|
||||
self.velocity += vel;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, dt: f32) {
|
||||
if self.fixed {
|
||||
self.velocity = Vec2::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
let pos = self.position;
|
||||
let vel = self.velocity;
|
||||
let acc = self.acceleration;
|
||||
|
||||
let new_pos = pos + vel * dt + acc * (dt * dt * 0.5);
|
||||
let new_vel = vel + acc * (dt * 0.5);
|
||||
|
||||
self.position = new_pos;
|
||||
self.velocity = new_vel * self.friction.powf(dt);
|
||||
self.acceleration = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeInfo {
|
||||
pub label: TextLayout,
|
||||
pub label_offset: Vec2,
|
||||
pub text_size: f32,
|
||||
pub hidden: bool,
|
||||
pub parent: Option<usize>,
|
||||
pub unhide: Vec<usize>,
|
||||
}
|
||||
|
||||
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,
|
||||
parent: None,
|
||||
unhide: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.unhide(parent);
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
pub fn build_force_graph(&self, graph: &mut ForceGraph, parent: usize) {
|
||||
let mut siblings = Vec::new();
|
||||
let mut first_sibling = None;
|
||||
let mut last_sibling = None;
|
||||
|
||||
const SIBLING_LENGTH: f32 = 60.0;
|
||||
const SIBLING_STRENGTH: f32 = 0.1;
|
||||
|
||||
for child in self.children.iter() {
|
||||
let id = graph.nodes.len();
|
||||
let _ = first_sibling.get_or_insert(id);
|
||||
|
||||
graph.nodes.push(Node {
|
||||
body: Body {
|
||||
position: Vec2::from_angle(id as f32),
|
||||
..Default::default()
|
||||
},
|
||||
info: NodeInfo {
|
||||
hidden: true,
|
||||
parent: Some(parent),
|
||||
..NodeInfo::new(&child.label)
|
||||
},
|
||||
});
|
||||
|
||||
graph.springs.push(Constraint {
|
||||
from: parent,
|
||||
to: id,
|
||||
length: 30.0,
|
||||
strength: 1.0,
|
||||
});
|
||||
|
||||
if let Some(last) = last_sibling {
|
||||
graph.springs.push(Constraint {
|
||||
from: last,
|
||||
to: id,
|
||||
length: SIBLING_LENGTH,
|
||||
strength: SIBLING_STRENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
siblings.push(id);
|
||||
last_sibling = Some(id);
|
||||
|
||||
graph.connections.push(Connection {
|
||||
from: parent,
|
||||
to: id,
|
||||
width: 1.0,
|
||||
color: Color::WHITE,
|
||||
});
|
||||
|
||||
child.build_force_graph(graph, id);
|
||||
}
|
||||
|
||||
if let (Some(first), Some(last)) = (first_sibling, last_sibling) {
|
||||
graph.springs.push(Constraint {
|
||||
from: first,
|
||||
to: last,
|
||||
length: SIBLING_LENGTH,
|
||||
strength: SIBLING_STRENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
graph.nodes[parent].info.unhide.extend_from_slice(&siblings);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Constraint {
|
||||
pub from: usize,
|
||||
pub to: usize,
|
||||
pub length: f32,
|
||||
pub strength: f32,
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn apply(&self, mut delta: Vec2) -> Vec2 {
|
||||
const ALPHA: f32 = 0.99;
|
||||
let distance = delta.length();
|
||||
let displacement = distance - self.length;
|
||||
delta *= displacement / distance * self.strength * ALPHA;
|
||||
delta / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Constraint>,
|
||||
pub connections: Vec<Connection>,
|
||||
}
|
||||
|
||||
impl ForceGraph {
|
||||
pub fn update(&mut self, dt: f32) {
|
||||
self.apply_springs(dt);
|
||||
self.apply_repulsion();
|
||||
|
||||
for node in self.nodes.iter_mut() {
|
||||
if !node.info.hidden {
|
||||
node.body.update(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reaction(&self, from: usize, to: usize, delta: Vec2) -> (Vec2, Vec2) {
|
||||
let from_fixed = self.nodes[from].body.fixed;
|
||||
let to_fixed = self.nodes[to].body.fixed;
|
||||
|
||||
if from_fixed {
|
||||
let from = Vec2::ZERO;
|
||||
let to = if to_fixed { Vec2::ZERO } else { -delta };
|
||||
(from, to)
|
||||
} else if to_fixed {
|
||||
let to = Vec2::ZERO;
|
||||
let from = if from_fixed { Vec2::ZERO } else { delta };
|
||||
(from, to)
|
||||
} else {
|
||||
let half_delta = delta / 2.0;
|
||||
(half_delta, -half_delta)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_unhidden<R>(
|
||||
&self,
|
||||
from: usize,
|
||||
to: usize,
|
||||
f: impl FnOnce(&Node, &Node) -> R,
|
||||
) -> Option<R> {
|
||||
let from = &self.nodes[from];
|
||||
let to = &self.nodes[to];
|
||||
|
||||
if from.info.hidden || to.info.hidden {
|
||||
None
|
||||
} else {
|
||||
Some(f(from, to))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_springs(&mut self, dt: f32) {
|
||||
for spring in self.springs.iter() {
|
||||
let from = spring.from;
|
||||
let to = spring.to;
|
||||
|
||||
let vel = self.with_unhidden(from, to, |from, to| {
|
||||
let from = from.body.position + from.body.velocity * dt;
|
||||
let to = to.body.position + to.body.velocity * dt;
|
||||
let delta = to - from;
|
||||
spring.apply(delta)
|
||||
});
|
||||
|
||||
if let Some(vel) = vel {
|
||||
let (from_vel, to_vel) = self.reaction(from, to, vel);
|
||||
self.nodes[from].body.apply_velocity(from_vel);
|
||||
self.nodes[to].body.apply_velocity(to_vel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_repulsion(&mut self) {
|
||||
for i in 1..self.nodes.len() {
|
||||
for j in i..self.nodes.len() {
|
||||
let from = i - 1;
|
||||
let to = j;
|
||||
|
||||
let force = self.with_unhidden(from, to, |from, to| {
|
||||
const REPULSION_CONSTANT: f32 = 1000.0;
|
||||
let delta = to.body.position - from.body.position;
|
||||
let proximity = delta.length().max(0.1);
|
||||
let force = -(REPULSION_CONSTANT / (proximity * proximity));
|
||||
delta * force
|
||||
});
|
||||
|
||||
if let Some(force) = force {
|
||||
let (from_force, to_force) = self.reaction(from, to, force);
|
||||
self.nodes[from].body.apply_force(from_force);
|
||||
self.nodes[to].body.apply_force(to_force);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unhide(&mut self, node: usize) {
|
||||
let node = &mut self.nodes[node];
|
||||
node.info.hidden = false;
|
||||
|
||||
let position = node.body.position;
|
||||
let children = node.info.unhide.to_owned();
|
||||
|
||||
// Avoid divide-by-zero
|
||||
if children.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut neighbor_num = children.len();
|
||||
let mut rotate_by = Vec2::new(1.0, 0.0);
|
||||
|
||||
if let Some(parent) = node.info.parent {
|
||||
neighbor_num += 1;
|
||||
rotate_by = self.nodes[parent].body.position - position;
|
||||
rotate_by = rotate_by.normalize();
|
||||
}
|
||||
|
||||
let theta = std::f32::consts::TAU / neighbor_num as f32;
|
||||
|
||||
for (index, child) in children.into_iter().enumerate() {
|
||||
let angle = Vec2::from_angle((index + 1) as f32 * theta).rotate(rotate_by);
|
||||
let child = &mut self.nodes[child];
|
||||
child.info.hidden = false;
|
||||
child.body.fixed = false;
|
||||
child.body.position = position + angle;
|
||||
child.body.velocity = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, node: usize) {
|
||||
let node = &mut self.nodes[node];
|
||||
node.info.hidden = true;
|
||||
|
||||
let children = node.info.unhide.to_owned();
|
||||
for child in children {
|
||||
self.hide(child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_select(&mut self, id: usize) {
|
||||
let node = &mut self.nodes[id];
|
||||
|
||||
if !node.body.fixed {
|
||||
node.body.fixed = true;
|
||||
self.unhide(id);
|
||||
} else {
|
||||
node.body.fixed = false;
|
||||
|
||||
let children = node.info.unhide.to_owned();
|
||||
for child in children {
|
||||
self.hide(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if node.info.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
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() {
|
||||
self.with_unhidden(connection.from, connection.to, |from, to| {
|
||||
let from = from.body.position;
|
||||
let to = to.body.position;
|
||||
let delta = to - from;
|
||||
|
||||
if delta.length() < radius * 2.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
center: Vec2,
|
||||
last_focus: Vec2,
|
||||
focus_anim: f32,
|
||||
focus_target: usize,
|
||||
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,
|
||||
last_focus: Vec2::ZERO,
|
||||
center: Vec2::ZERO,
|
||||
focus_anim: 1.0,
|
||||
focus_target: 0,
|
||||
size: Vec2::splat(100.0),
|
||||
};
|
||||
Box::new(panel)
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for ForceDirectedGraphPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
self.graph.update(dt);
|
||||
self.focus_anim += dt;
|
||||
|
||||
let target_pos = self.graph.nodes[self.focus_target].body.position;
|
||||
let anim = self.focus_anim / 0.5;
|
||||
if anim < 0.0 {
|
||||
self.center = self.last_focus;
|
||||
} else if anim < 1.0 {
|
||||
let anim = -(anim * (anim - 2.0));
|
||||
self.center = target_pos * anim + self.last_focus * (1.0 - anim);
|
||||
} else {
|
||||
self.center = target_pos;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let offset = self.size / 2.0 - self.center;
|
||||
let ctx = DrawContext::new(self.panel).with_offset(offset);
|
||||
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) {
|
||||
let offset = self.size / 2.0 - self.center;
|
||||
let at = at - offset;
|
||||
if let CursorEventKind::Deselect = kind {
|
||||
let node = self
|
||||
.graph
|
||||
.nodes
|
||||
.iter()
|
||||
.position(|node| !node.info.hidden && node.body.position.distance(at) < 6.0);
|
||||
if let Some(node) = node {
|
||||
self.graph.on_select(node);
|
||||
self.last_focus = self.center;
|
||||
self.focus_target = node;
|
||||
self.focus_anim = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {}
|
||||
}
|
Loading…
Reference in New Issue