Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
mars | b0ecf1eb92 | |
mars | 1fb7f03468 | |
mars | 9318b59ac0 | |
mars | 1f2242c36c | |
mars | 47f0288f21 | |
mars | 247028caa8 | |
mars | 68fd63eaff | |
mars | 51546fc2a2 | |
mars | 1803d784f9 | |
mars | be6693b20a | |
mars | 36edf051ff | |
mars | 1b3a991144 | |
mars | 7de349e60a | |
mars | 180cd516ea | |
mars | bb72e6d753 | |
mars | 4f3363fc0f | |
mars | 13142ee210 | |
mars | eb5eb4f76d | |
Skye Terran | d3f9f81468 |
|
@ -1,6 +1,8 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"editor"
|
||||
"editor",
|
||||
"ramen",
|
||||
"ramen_egui"
|
||||
]
|
||||
|
||||
[package]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "ramen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
glam = { version = "0.20", features = ["serde"] }
|
||||
slab = "^0.4"
|
|
@ -0,0 +1,239 @@
|
|||
use super::{Graph, GraphError, GraphResult};
|
||||
use crate::node::{Edge, Node, SlotIndex};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Command: Debug {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand>;
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()>;
|
||||
}
|
||||
|
||||
pub type DynCommand = Box<dyn Command>;
|
||||
|
||||
pub struct CommandHistory {
|
||||
pub(crate) commands: Vec<(DynCommand, DynCommand)>,
|
||||
pub(crate) cursor: usize,
|
||||
}
|
||||
|
||||
impl CommandHistory {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commands: Vec::new(),
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, graph: &mut Graph, cmd: DynCommand) -> GraphResult<()> {
|
||||
let undo = cmd.undo(graph)?;
|
||||
cmd.apply(graph)?;
|
||||
|
||||
// Discard redo history on new push
|
||||
if self.cursor < self.commands.len() {
|
||||
self.commands.truncate(self.cursor);
|
||||
}
|
||||
|
||||
self.commands.push((cmd, undo));
|
||||
self.cursor += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, graph: &mut Graph) -> GraphResult<bool> {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
self.commands[self.cursor].1.apply(graph)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, graph: &mut Graph) -> GraphResult<bool> {
|
||||
if self.cursor < self.commands.len() {
|
||||
self.commands[self.cursor].0.apply(graph)?;
|
||||
self.cursor += 1;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateNode {
|
||||
pub kind: usize,
|
||||
pub pos: glam::Vec2,
|
||||
pub inputs: Vec<Option<SlotIndex>>,
|
||||
}
|
||||
|
||||
impl Command for CreateNode {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
|
||||
let target = graph.nodes.vacant_key();
|
||||
let undo = DeleteNode { target };
|
||||
Ok(Box::new(undo))
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
|
||||
let node = Node {
|
||||
kind: self.kind,
|
||||
pos: self.pos,
|
||||
inputs: self.inputs.clone(),
|
||||
};
|
||||
|
||||
graph.nodes.insert(node);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Node> for CreateNode {
|
||||
fn from(node: Node) -> Self {
|
||||
Self {
|
||||
kind: node.kind,
|
||||
pos: node.pos,
|
||||
inputs: node.inputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeleteNode {
|
||||
pub target: usize,
|
||||
}
|
||||
|
||||
impl Command for DeleteNode {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
|
||||
let target = graph.get_node(self.target)?;
|
||||
|
||||
let undo = CreateNode {
|
||||
kind: target.kind,
|
||||
pos: target.pos,
|
||||
inputs: target.inputs.clone(),
|
||||
};
|
||||
|
||||
Ok(Box::new(undo))
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
|
||||
graph
|
||||
.nodes
|
||||
.try_remove(self.target)
|
||||
.ok_or(GraphError::InvalidReference)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MoveNode {
|
||||
pub target: usize,
|
||||
pub to: glam::Vec2,
|
||||
pub relative: bool,
|
||||
}
|
||||
|
||||
impl Command for MoveNode {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
|
||||
let target = graph.get_node(self.target)?;
|
||||
|
||||
let undo = Self {
|
||||
target: self.target,
|
||||
to: target.pos,
|
||||
relative: false,
|
||||
};
|
||||
|
||||
Ok(Box::new(undo))
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
|
||||
let target = graph.get_node_mut(self.target)?;
|
||||
|
||||
if self.relative {
|
||||
target.pos += self.to;
|
||||
} else {
|
||||
target.pos = self.to;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SetEdge {
|
||||
pub edge: Edge,
|
||||
}
|
||||
|
||||
impl Command for SetEdge {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
|
||||
let target = graph.get_node(self.edge.output.node)?;
|
||||
|
||||
let old_input = target
|
||||
.inputs
|
||||
.get(self.edge.output.slot)
|
||||
.ok_or(GraphError::InvalidReference)?;
|
||||
|
||||
Ok(match old_input {
|
||||
Some(old_input) => Box::new(SetEdge {
|
||||
edge: Edge {
|
||||
input: *old_input,
|
||||
output: self.edge.output,
|
||||
},
|
||||
}),
|
||||
None => Box::new(DeleteEdge {
|
||||
input: self.edge.output,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
|
||||
let target = graph.get_node_mut(self.edge.output.node)?;
|
||||
|
||||
let input = target
|
||||
.inputs
|
||||
.get_mut(self.edge.output.slot)
|
||||
.ok_or(GraphError::InvalidReference)?;
|
||||
|
||||
*input = Some(self.edge.input);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeleteEdge {
|
||||
pub input: SlotIndex,
|
||||
}
|
||||
|
||||
impl Command for DeleteEdge {
|
||||
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
|
||||
let target = graph.get_node(self.input.node)?;
|
||||
|
||||
let old_input = target
|
||||
.inputs
|
||||
.get(self.input.slot)
|
||||
.ok_or(GraphError::InvalidReference)?
|
||||
.ok_or(GraphError::MissingEdge)?;
|
||||
|
||||
let undo = SetEdge {
|
||||
edge: Edge {
|
||||
input: old_input,
|
||||
output: self.input,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Box::new(undo))
|
||||
}
|
||||
|
||||
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
|
||||
let target = graph.get_node_mut(self.input.node)?;
|
||||
|
||||
let input = target
|
||||
.inputs
|
||||
.get_mut(self.input.slot)
|
||||
.ok_or(GraphError::InvalidReference)?;
|
||||
|
||||
if input.is_none() {
|
||||
Err(GraphError::MissingEdge)
|
||||
} else {
|
||||
*input = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
use slab::Slab;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod command;
|
||||
pub mod node;
|
||||
pub mod node_kind;
|
||||
|
||||
use command::*;
|
||||
use node::{Edge, Node, SlotIndex};
|
||||
use node_kind::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GraphError {
|
||||
InvalidReference,
|
||||
MissingEdge,
|
||||
Cyclic,
|
||||
}
|
||||
|
||||
pub type GraphResult<T> = Result<T, GraphError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Graph {
|
||||
pub node_kinds: Arc<NodeKindStore>,
|
||||
pub nodes: Slab<Node>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn new(node_kinds: Arc<NodeKindStore>) -> Self {
|
||||
Self {
|
||||
node_kinds,
|
||||
nodes: Slab::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_node(&self, index: usize) -> GraphResult<&Node> {
|
||||
self.nodes.get(index).ok_or(GraphError::InvalidReference)
|
||||
}
|
||||
|
||||
pub fn get_node_mut(&mut self, index: usize) -> GraphResult<&mut Node> {
|
||||
self.nodes
|
||||
.get_mut(index)
|
||||
.ok_or(GraphError::InvalidReference)
|
||||
}
|
||||
|
||||
// Return the edges that can be derived from the graph
|
||||
pub fn edges(&self) -> Result<Vec<Edge>, GraphError> {
|
||||
let topo_order = self.topological_order()?;
|
||||
|
||||
let mut edges = Vec::new();
|
||||
for (node_idx, node) in topo_order.iter().enumerate() {
|
||||
for (slot_idx, input) in self.nodes[*node].inputs.iter().enumerate() {
|
||||
if let Some(input) = input {
|
||||
let output = SlotIndex {
|
||||
node: node_idx,
|
||||
slot: slot_idx,
|
||||
};
|
||||
|
||||
edges.push(Edge {
|
||||
input: *input,
|
||||
output,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(edges)
|
||||
}
|
||||
|
||||
pub fn topological_order(&self) -> Result<Vec<usize>, GraphError> {
|
||||
// The number of nodes
|
||||
let v = self.nodes.len();
|
||||
|
||||
// Forward-directed adjacency lists
|
||||
let mut adjacency_lists: Vec<Vec<usize>> = vec![Vec::new(); v];
|
||||
for (i, node) in self.nodes.iter() {
|
||||
for j in node.inputs.iter().flatten() {
|
||||
adjacency_lists[j.node].push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Create list of indegrees
|
||||
let mut indegrees: Vec<usize> = vec![0; v];
|
||||
for (i, node) in self.nodes.iter() {
|
||||
indegrees[i] = node.inputs.len();
|
||||
}
|
||||
|
||||
// Create a queue and initialize it with nodes with no indegrees
|
||||
let mut queue: Vec<usize> = Vec::new();
|
||||
for (i, count) in indegrees.iter().enumerate() {
|
||||
if *count == 0 {
|
||||
queue.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
let mut sort_count = 0;
|
||||
let mut topo_order: Vec<usize> = Vec::new();
|
||||
|
||||
while !queue.is_empty() {
|
||||
let u = queue.pop().unwrap();
|
||||
topo_order.push(u);
|
||||
|
||||
for i in &adjacency_lists[u] {
|
||||
indegrees[*i] -= 1;
|
||||
if indegrees[*i] == 0 {
|
||||
queue.push(*i);
|
||||
}
|
||||
}
|
||||
|
||||
sort_count += 1;
|
||||
}
|
||||
|
||||
// Check if there was a cycle
|
||||
if sort_count != v {
|
||||
Err(GraphError::Cyclic)
|
||||
} else {
|
||||
Ok(topo_order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TestOpKind {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
}
|
||||
|
||||
pub struct TestGraphBuilder {
|
||||
pub graph: Graph,
|
||||
pub literal_kind: usize,
|
||||
pub add_kind: usize,
|
||||
pub sub_kind: usize,
|
||||
pub mul_kind: usize,
|
||||
pub div_kind: usize,
|
||||
}
|
||||
|
||||
impl TestGraphBuilder {
|
||||
pub fn new() -> Self {
|
||||
let mut kinds = NodeKindStore { kinds: vec![] };
|
||||
|
||||
let literal_kind = kinds.push(NodeKind {
|
||||
title: "Literal".into(),
|
||||
inputs: vec![],
|
||||
outputs: vec![NodeKindOutput { label: "".into() }],
|
||||
});
|
||||
|
||||
let mut make_arith_kind = |title: &str| -> usize {
|
||||
kinds.push(NodeKind {
|
||||
title: title.to_string(),
|
||||
inputs: vec![
|
||||
NodeKindInput {
|
||||
label: "A".to_string(),
|
||||
},
|
||||
NodeKindInput {
|
||||
label: "B".to_string(),
|
||||
},
|
||||
],
|
||||
outputs: vec![NodeKindOutput {
|
||||
label: "Result".to_string(),
|
||||
}],
|
||||
})
|
||||
};
|
||||
|
||||
let add_kind = make_arith_kind("Add");
|
||||
let sub_kind = make_arith_kind("Subtract");
|
||||
let mul_kind = make_arith_kind("Multiply");
|
||||
let div_kind = make_arith_kind("Divide");
|
||||
|
||||
let graph = Graph::new(Arc::new(kinds));
|
||||
|
||||
Self {
|
||||
graph,
|
||||
literal_kind,
|
||||
add_kind,
|
||||
sub_kind,
|
||||
mul_kind,
|
||||
div_kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, node: Node) -> usize {
|
||||
self.graph.nodes.insert(node)
|
||||
}
|
||||
|
||||
pub fn add_literal(&mut self, value: f32) -> usize {
|
||||
self.graph.nodes.insert(self.make_literal(value))
|
||||
}
|
||||
|
||||
pub fn make_literal(&self, _value: f32) -> Node {
|
||||
Node {
|
||||
kind: self.literal_kind,
|
||||
pos: glam::Vec2::ZERO,
|
||||
inputs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_op(&mut self, kind: TestOpKind, a: usize, b: usize) -> usize {
|
||||
self.graph.nodes.insert(self.make_op(kind, a, b))
|
||||
}
|
||||
|
||||
pub fn make_op(&self, kind: TestOpKind, a: usize, b: usize) -> Node {
|
||||
let kind = match kind {
|
||||
TestOpKind::Add => self.add_kind,
|
||||
TestOpKind::Sub => self.sub_kind,
|
||||
TestOpKind::Mul => self.mul_kind,
|
||||
TestOpKind::Div => self.div_kind,
|
||||
};
|
||||
|
||||
Node {
|
||||
kind,
|
||||
pos: glam::Vec2::ZERO,
|
||||
inputs: vec![
|
||||
Some(SlotIndex { node: a, slot: 0 }),
|
||||
Some(SlotIndex { node: b, slot: 0 }),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn three_node_add_cmds(&self) -> Vec<DynCommand> {
|
||||
vec![
|
||||
Box::new(CreateNode::from(self.make_literal(1.0))),
|
||||
Box::new(CreateNode::from(self.make_literal(2.0))),
|
||||
Box::new(CreateNode::from(Node {
|
||||
kind: self.add_kind,
|
||||
pos: glam::Vec2::ZERO,
|
||||
inputs: vec![None; 2],
|
||||
})),
|
||||
Box::new(SetEdge {
|
||||
edge: Edge {
|
||||
input: SlotIndex { node: 0, slot: 0 },
|
||||
output: SlotIndex { node: 2, slot: 0 },
|
||||
},
|
||||
}),
|
||||
Box::new(SetEdge {
|
||||
edge: Edge {
|
||||
input: SlotIndex { node: 1, slot: 0 },
|
||||
output: SlotIndex { node: 2, slot: 1 },
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl CommandHistory {
|
||||
pub fn push_multi(
|
||||
&mut self,
|
||||
graph: &mut Graph,
|
||||
cmds: impl IntoIterator<Item = DynCommand>,
|
||||
) -> GraphResult<()> {
|
||||
for cmd in cmds {
|
||||
println!("{:#?}", cmd);
|
||||
self.push(graph, cmd)?;
|
||||
println!("{:#?}", graph);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rewind(&mut self, graph: &mut Graph) -> GraphResult<()> {
|
||||
while self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
let undo = &self.commands[self.cursor].1;
|
||||
println!("{:#?}", undo);
|
||||
undo.apply(graph)?;
|
||||
println!("{:#?}", graph);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_node() -> GraphResult<()> {
|
||||
let mut builder = TestGraphBuilder::new();
|
||||
|
||||
let cmd = CreateNode {
|
||||
kind: builder.literal_kind,
|
||||
pos: glam::Vec2::ZERO,
|
||||
inputs: vec![],
|
||||
};
|
||||
|
||||
let undo = cmd.undo(&builder.graph)?;
|
||||
|
||||
cmd.apply(&mut builder.graph)?;
|
||||
assert_eq!(builder.graph.nodes.len(), 1);
|
||||
|
||||
undo.apply(&mut builder.graph)?;
|
||||
assert_eq!(builder.graph.nodes.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_node_add() -> GraphResult<()> {
|
||||
let mut builder = TestGraphBuilder::new();
|
||||
let cmds = builder.three_node_add_cmds();
|
||||
|
||||
for cmd in cmds.into_iter() {
|
||||
println!("{:#?}", cmd);
|
||||
cmd.apply(&mut builder.graph)?;
|
||||
println!("{:#?}", builder.graph);
|
||||
}
|
||||
|
||||
builder.graph.topological_order()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_node_add_rewind() -> GraphResult<()> {
|
||||
let mut builder = TestGraphBuilder::new();
|
||||
let mut history = CommandHistory::new();
|
||||
let cmds = builder.three_node_add_cmds();
|
||||
history.push_multi(&mut builder.graph, cmds)?;
|
||||
builder.graph.topological_order()?;
|
||||
history.rewind(&mut builder.graph)?;
|
||||
assert_eq!(builder.graph.nodes.len(), 0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
use crate::{GraphError, GraphResult};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Node {
|
||||
pub kind: usize,
|
||||
pub pos: glam::Vec2,
|
||||
pub inputs: Vec<Option<SlotIndex>>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn unwrap_inputs(&self) -> GraphResult<Vec<SlotIndex>> {
|
||||
let mut inputs = Vec::with_capacity(self.inputs.len());
|
||||
|
||||
for input in self.inputs.iter() {
|
||||
if let Some(input) = input {
|
||||
inputs.push(*input);
|
||||
} else {
|
||||
return Err(GraphError::MissingEdge);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(inputs)
|
||||
}
|
||||
|
||||
pub fn at(self, pos: glam::Vec2) -> Self {
|
||||
Self {
|
||||
pos,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct SlotIndex {
|
||||
pub node: usize,
|
||||
pub slot: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Edge {
|
||||
pub input: SlotIndex,
|
||||
pub output: SlotIndex,
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
#[derive(Debug)]
|
||||
pub struct NodeKindStore {
|
||||
pub kinds: Vec<NodeKind>,
|
||||
}
|
||||
|
||||
impl NodeKindStore {
|
||||
pub fn push(&mut self, kind: NodeKind) -> usize {
|
||||
let idx = self.kinds.len();
|
||||
self.kinds.push(kind);
|
||||
idx
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeKind {
|
||||
pub title: String,
|
||||
pub inputs: Vec<NodeKindInput>,
|
||||
pub outputs: Vec<NodeKindOutput>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeKindInput {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeKindOutput {
|
||||
pub label: String,
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "ramen_egui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
egui = "0.18"
|
||||
glam = "0.20"
|
||||
ramen = { path = "../ramen" }
|
||||
|
||||
[dev-dependencies]
|
||||
eframe = "0.18"
|
|
@ -0,0 +1,61 @@
|
|||
use eframe::egui;
|
||||
use ramen::{TestGraphBuilder, TestOpKind};
|
||||
use ramen_egui::NodeEditor;
|
||||
|
||||
fn main() {
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
eframe::run_native(
|
||||
"Ramen example",
|
||||
native_options,
|
||||
Box::new(|cc| Box::new(Application::new(cc))),
|
||||
);
|
||||
}
|
||||
|
||||
struct Application {
|
||||
editor: NodeEditor,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
|
||||
let scale = 250.0;
|
||||
let pos = |x, y| glam::Vec2::new(x, y) * scale;
|
||||
|
||||
let mut builder = TestGraphBuilder::new();
|
||||
let n0 = builder.add(builder.make_literal(1.0).at(pos(1.0, 1.0)));
|
||||
let n1 = builder.add(builder.make_literal(2.0).at(pos(1.0, 2.0)));
|
||||
let n2 = builder.add(builder.make_op(TestOpKind::Add, n0, n1).at(pos(2.0, 1.5)));
|
||||
let n3 = builder.add(builder.make_literal(4.0).at(pos(2.0, 2.5)));
|
||||
let n4 = builder.add(builder.make_op(TestOpKind::Div, n2, n3).at(pos(3.0, 2.0)));
|
||||
let n5 = builder.add(builder.make_op(TestOpKind::Mul, n4, n4).at(pos(4.0, 2.0)));
|
||||
let _n6 = builder.add(builder.make_op(TestOpKind::Add, n5, n0).at(pos(3.0, 1.0)));
|
||||
|
||||
Self {
|
||||
editor: NodeEditor::from_graph(builder.graph),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Application {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::top("menu_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::widgets::global_dark_light_mode_switch(ui);
|
||||
|
||||
if ui.button("Undo").clicked() {
|
||||
self.editor.undo();
|
||||
}
|
||||
|
||||
if ui.button("Redo").clicked() {
|
||||
self.editor.redo();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Hello world!");
|
||||
self.editor.show(ui);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,472 @@
|
|||
use egui::{Color32, Frame, Stroke};
|
||||
use glam::Vec2;
|
||||
use ramen::command::CommandHistory;
|
||||
use ramen::node::{Edge, SlotIndex};
|
||||
use ramen::Graph;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct NodeEditor {
|
||||
graph: Graph,
|
||||
cmd_history: CommandHistory,
|
||||
pointer_state: PointerState,
|
||||
}
|
||||
|
||||
impl NodeEditor {
|
||||
pub fn from_graph(graph: Graph) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
cmd_history: CommandHistory::new(),
|
||||
pointer_state: PointerState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) {
|
||||
self.cmd_history.undo(&mut self.graph).unwrap();
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
self.cmd_history.redo(&mut self.graph).unwrap();
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||
let style = ui.style();
|
||||
let style = GraphStyle::from_ui_style(style);
|
||||
let layout = GraphLayout::layout(ui, &style, &self.pointer_state, &self.graph);
|
||||
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let sense = egui::Sense::click_and_drag();
|
||||
let response = ui.allocate_rect(rect, sense);
|
||||
|
||||
let pointer_pos = response.interact_pointer_pos();
|
||||
let pointer_target = pointer_pos.map(|pos| layout.pointer_target(pos));
|
||||
let painter = ui.painter_at(rect);
|
||||
layout.paint(rect, &painter);
|
||||
|
||||
if let Some(pointer_pos) = pointer_pos {
|
||||
self.pointer_state.position = pointer_pos;
|
||||
self.pointer_state.delta = response.drag_delta();
|
||||
|
||||
if let PointerAction::Idle(idle) = &mut self.pointer_state.action {
|
||||
*idle = pointer_target.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if response.drag_started() && response.dragged_by(egui::PointerButton::Primary) {
|
||||
let pointer_target = pointer_target.expect("Drag started but no target");
|
||||
self.pointer_state.start = self.pointer_state.position;
|
||||
|
||||
let next_state = match pointer_target {
|
||||
PointerTarget::Grid => None, // TODO grid navigation
|
||||
PointerTarget::Node(idx) => Some(PointerAction::MovingNode(idx)),
|
||||
PointerTarget::Input(idx) => Some(PointerAction::SettingInput(idx)),
|
||||
PointerTarget::Output(idx) => Some(PointerAction::SettingOutput(idx)),
|
||||
};
|
||||
|
||||
if let Some(next_state) = next_state {
|
||||
self.pointer_state.action = next_state;
|
||||
}
|
||||
} else if response.drag_released() {
|
||||
use ramen::command::*;
|
||||
|
||||
let cmd: Option<DynCommand> = match self.pointer_state.action {
|
||||
PointerAction::Idle(_) => None,
|
||||
PointerAction::MovingNode(idx) => {
|
||||
let delta = self.pointer_state.position - self.pointer_state.start;
|
||||
Some(Box::new(MoveNode {
|
||||
target: idx,
|
||||
to: Vec2::new(delta.x, delta.y),
|
||||
relative: true,
|
||||
}))
|
||||
}
|
||||
PointerAction::SettingInput(output) => {
|
||||
if let Some(PointerTarget::Output(input)) = pointer_target {
|
||||
Some(Box::new(SetEdge {
|
||||
edge: Edge { input, output },
|
||||
}))
|
||||
} else if self.graph.nodes[output.node].inputs[output.slot].is_some() {
|
||||
Some(Box::new(DeleteEdge { input: output }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
PointerAction::SettingOutput(input) => {
|
||||
if let Some(PointerTarget::Input(output)) = pointer_target {
|
||||
Some(Box::new(SetEdge {
|
||||
edge: Edge { input, output },
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cmd) = cmd {
|
||||
let cmd_debug = format!("{:#?}", cmd);
|
||||
|
||||
match self.cmd_history.push(&mut self.graph, cmd) {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("{}:\n {:#?}", cmd_debug, e),
|
||||
}
|
||||
}
|
||||
|
||||
self.pointer_state.action = PointerAction::Idle(None); // TODO idle pointer targeting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PointerState {
|
||||
pub start: egui::Pos2,
|
||||
pub position: egui::Pos2,
|
||||
pub delta: egui::Vec2,
|
||||
pub action: PointerAction,
|
||||
}
|
||||
|
||||
impl PointerState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
start: egui::Pos2::ZERO,
|
||||
position: egui::Pos2::ZERO,
|
||||
delta: egui::Vec2::ZERO,
|
||||
action: PointerAction::Idle(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum PointerAction {
|
||||
Idle(Option<PointerTarget>),
|
||||
MovingNode(usize),
|
||||
SettingInput(SlotIndex),
|
||||
SettingOutput(SlotIndex),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum PointerTarget {
|
||||
Grid,
|
||||
Node(usize),
|
||||
Input(SlotIndex),
|
||||
Output(SlotIndex),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EdgeLayout {
|
||||
pub points: [Vec2; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GraphStyle {
|
||||
pub grid_spacing: f32,
|
||||
pub grid_stroke: Stroke,
|
||||
pub node_frame: Frame,
|
||||
pub edge_stroke: Stroke,
|
||||
pub edge_control_offset: f32,
|
||||
pub slot_radius: f32,
|
||||
pub slot_spacing: f32,
|
||||
pub slot_label_margin: f32,
|
||||
pub slot_fill: Color32,
|
||||
pub slot_stroke: Stroke,
|
||||
}
|
||||
|
||||
impl GraphStyle {
|
||||
pub fn from_ui_style(style: &egui::Style) -> Self {
|
||||
let node_alpha = 192;
|
||||
|
||||
let node_bg = if style.visuals.dark_mode {
|
||||
Color32::from_black_alpha(node_alpha)
|
||||
} else {
|
||||
Color32::from_white_alpha(node_alpha)
|
||||
};
|
||||
|
||||
Self {
|
||||
grid_spacing: 25.0,
|
||||
grid_stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||
node_frame: Frame::window(style).fill(node_bg),
|
||||
edge_stroke: style.visuals.widgets.active.fg_stroke,
|
||||
edge_control_offset: 200.0,
|
||||
slot_radius: 7.0,
|
||||
slot_spacing: 40.0,
|
||||
slot_label_margin: 5.0,
|
||||
slot_fill: style.visuals.widgets.noninteractive.bg_fill,
|
||||
slot_stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextLayout {
|
||||
pub pos: egui::Pos2,
|
||||
pub galley: Arc<egui::Galley>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GraphLayout {
|
||||
pub style: GraphStyle,
|
||||
pub node_rects: HashMap<usize, egui::Rect>,
|
||||
pub input_positions: HashMap<SlotIndex, Vec2>,
|
||||
pub output_positions: HashMap<SlotIndex, Vec2>,
|
||||
pub edges: HashMap<Edge, EdgeLayout>,
|
||||
pub active_edge: Option<(Vec2, Vec2)>,
|
||||
pub texts: Vec<TextLayout>,
|
||||
}
|
||||
|
||||
impl GraphLayout {
|
||||
pub fn layout(
|
||||
ui: &egui::Ui,
|
||||
style: &GraphStyle,
|
||||
pointer: &PointerState,
|
||||
graph: &Graph,
|
||||
) -> Self {
|
||||
let mut layout = Self {
|
||||
style: style.clone(),
|
||||
node_rects: Default::default(),
|
||||
input_positions: Default::default(),
|
||||
output_positions: Default::default(),
|
||||
edges: Default::default(),
|
||||
active_edge: None,
|
||||
texts: Default::default(),
|
||||
};
|
||||
|
||||
let mut edges = Vec::new();
|
||||
|
||||
let kind_font = egui::FontSelection::Style(egui::TextStyle::Heading);
|
||||
let kind_font = kind_font.resolve(ui.style());
|
||||
let kind_spacing = ui.style().spacing.item_spacing.y;
|
||||
let kind_color = ui.visuals().text_color();
|
||||
let label_font = egui::FontSelection::Style(egui::TextStyle::Body);
|
||||
let label_font = label_font.resolve(ui.style());
|
||||
let label_color = ui.visuals().text_color();
|
||||
|
||||
for (node_index, node) in graph.nodes.iter() {
|
||||
let node_kind = &graph.node_kinds.kinds[node.kind];
|
||||
let mut node_pos = egui::pos2(node.pos.x, node.pos.y);
|
||||
|
||||
if pointer.action == PointerAction::MovingNode(node_index) {
|
||||
node_pos += pointer.position - pointer.start;
|
||||
}
|
||||
|
||||
let node_size = egui::vec2(150., 150.);
|
||||
let node_rect = egui::Rect::from_min_size(node_pos, node_size);
|
||||
layout.node_rects.insert(node_index, node_rect);
|
||||
|
||||
let text = node_kind.title.to_string();
|
||||
let kind_font = kind_font.clone();
|
||||
let wrap_width = f32::MAX;
|
||||
let galley = ui.fonts().layout(text, kind_font, kind_color, wrap_width);
|
||||
|
||||
let kind_height = galley.rect.height() + kind_spacing * 2.0;
|
||||
|
||||
layout.texts.push(TextLayout {
|
||||
pos: egui::pos2(
|
||||
node_rect.center().x - galley.rect.center().x,
|
||||
node_pos.y + kind_height - galley.rect.height(),
|
||||
),
|
||||
galley,
|
||||
});
|
||||
|
||||
let slot_top = node_pos.y + kind_height + style.slot_spacing;
|
||||
let slot_left = node_rect.left();
|
||||
let slot_right = node_rect.right();
|
||||
|
||||
for (slot_index, input) in node.inputs.iter().enumerate() {
|
||||
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
|
||||
let position = glam::Vec2::new(slot_left, slot_y);
|
||||
|
||||
let slot_index = SlotIndex {
|
||||
node: node_index,
|
||||
slot: slot_index,
|
||||
};
|
||||
|
||||
layout.input_positions.insert(slot_index, position);
|
||||
|
||||
let text = node_kind.inputs[slot_index.slot].label.to_owned();
|
||||
let label_font = label_font.clone();
|
||||
let wrap_width = f32::MAX;
|
||||
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
|
||||
layout.texts.push(TextLayout {
|
||||
pos: egui::pos2(
|
||||
position.x - galley.rect.left()
|
||||
+ style.slot_radius
|
||||
+ style.slot_label_margin,
|
||||
position.y - galley.rect.bottom() + style.slot_radius,
|
||||
),
|
||||
galley,
|
||||
});
|
||||
|
||||
if pointer.action == PointerAction::SettingInput(slot_index) {
|
||||
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
|
||||
layout.active_edge = Some((position, pointer));
|
||||
} else if let Some(input) = input {
|
||||
edges.push(Edge {
|
||||
input: *input,
|
||||
output: slot_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (slot_index, output) in node_kind.outputs.iter().enumerate() {
|
||||
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
|
||||
let position = glam::Vec2::new(slot_right, slot_y);
|
||||
|
||||
let slot_index = SlotIndex {
|
||||
node: node_index,
|
||||
slot: slot_index,
|
||||
};
|
||||
|
||||
layout.output_positions.insert(slot_index, position);
|
||||
|
||||
let text = output.label.to_owned();
|
||||
let label_font = label_font.clone();
|
||||
let wrap_width = f32::MAX;
|
||||
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
|
||||
layout.texts.push(TextLayout {
|
||||
pos: egui::pos2(
|
||||
position.x
|
||||
- galley.rect.right()
|
||||
- style.slot_radius
|
||||
- style.slot_label_margin,
|
||||
position.y - galley.rect.bottom() + style.slot_radius,
|
||||
),
|
||||
galley,
|
||||
});
|
||||
|
||||
if pointer.action == PointerAction::SettingOutput(slot_index) {
|
||||
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
|
||||
layout.active_edge = Some((pointer, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for edge in edges.into_iter() {
|
||||
let input = *layout.input_positions.get(&edge.output).unwrap();
|
||||
let output = *layout.output_positions.get(&edge.input).unwrap();
|
||||
let edge_layout = layout.layout_edge(input, output);
|
||||
layout.edges.insert(edge, edge_layout);
|
||||
}
|
||||
|
||||
layout
|
||||
}
|
||||
|
||||
pub fn layout_edge(&self, input: Vec2, output: Vec2) -> EdgeLayout {
|
||||
let control_offset = Vec2::new(self.style.edge_control_offset, 0.0);
|
||||
let mut input_control = input - control_offset;
|
||||
let mut output_control = output + control_offset;
|
||||
|
||||
if output_control.x > input_control.x && output.x < input.x {
|
||||
let mid = (output_control.x + input_control.x) / 2.0;
|
||||
output_control.x = mid;
|
||||
input_control.x = mid;
|
||||
}
|
||||
|
||||
EdgeLayout {
|
||||
points: [input, input_control, output_control, output],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pointer_target(&self, pointer: egui::Pos2) -> PointerTarget {
|
||||
let test_slots = |positions: &HashMap<SlotIndex, Vec2>| -> Option<SlotIndex> {
|
||||
for (index, position) in positions.iter() {
|
||||
let dx = pointer.x - position.x;
|
||||
let dy = pointer.y - position.y;
|
||||
let d2 = (dx * dx) + (dy * dy);
|
||||
let slot_radius2 = self.style.slot_radius.powi(2);
|
||||
|
||||
if d2 < slot_radius2 {
|
||||
return Some(*index);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(index) = test_slots(&self.input_positions) {
|
||||
return PointerTarget::Input(index);
|
||||
}
|
||||
|
||||
if let Some(index) = test_slots(&self.output_positions) {
|
||||
return PointerTarget::Output(index);
|
||||
}
|
||||
|
||||
for (node_index, node_rect) in self.node_rects.iter() {
|
||||
if node_rect.contains(pointer) {
|
||||
return PointerTarget::Node(*node_index);
|
||||
}
|
||||
}
|
||||
|
||||
PointerTarget::Grid
|
||||
}
|
||||
|
||||
pub fn paint(&self, rect: egui::Rect, painter: &egui::Painter) {
|
||||
let style = &self.style;
|
||||
let grid_spacing = style.grid_spacing;
|
||||
let grid_pos = rect.left_top().to_vec2() / egui::vec2(grid_spacing, grid_spacing);
|
||||
let mut grid_pos = grid_pos.floor() * grid_spacing;
|
||||
|
||||
while grid_pos.x < rect.right() {
|
||||
grid_pos.x += grid_spacing;
|
||||
let start = egui::pos2(grid_pos.x, rect.top());
|
||||
let end = egui::pos2(grid_pos.x, rect.bottom());
|
||||
painter.line_segment([start, end], style.grid_stroke);
|
||||
}
|
||||
|
||||
while grid_pos.y < rect.bottom() {
|
||||
grid_pos.y += grid_spacing;
|
||||
let start = egui::pos2(rect.left(), grid_pos.y);
|
||||
let end = egui::pos2(rect.right(), grid_pos.y);
|
||||
painter.line_segment([start, end], style.grid_stroke);
|
||||
}
|
||||
|
||||
let paint_edge = |layout: &EdgeLayout| {
|
||||
let points = layout.points.map(|p| p.to_array().into());
|
||||
|
||||
let shape = egui::epaint::CubicBezierShape {
|
||||
points,
|
||||
closed: false,
|
||||
fill: egui::Color32::TRANSPARENT,
|
||||
stroke: style.edge_stroke,
|
||||
};
|
||||
|
||||
painter.add(shape);
|
||||
};
|
||||
|
||||
for (_edge, edge_layout) in self.edges.iter() {
|
||||
paint_edge(edge_layout);
|
||||
}
|
||||
|
||||
if let Some(active_edge) = self.active_edge {
|
||||
let layout = self.layout_edge(active_edge.0, active_edge.1);
|
||||
paint_edge(&layout);
|
||||
}
|
||||
|
||||
for (_node_index, node_rect) in self.node_rects.iter() {
|
||||
let shape = style.node_frame.paint(*node_rect);
|
||||
painter.add(shape);
|
||||
}
|
||||
|
||||
let draw_slots = |positions: &HashMap<SlotIndex, Vec2>| {
|
||||
for (_index, position) in positions.iter() {
|
||||
let shape = egui::epaint::CircleShape {
|
||||
center: egui::pos2(position.x, position.y),
|
||||
radius: style.slot_radius,
|
||||
fill: style.slot_fill,
|
||||
stroke: style.slot_stroke,
|
||||
};
|
||||
|
||||
painter.add(shape);
|
||||
}
|
||||
};
|
||||
|
||||
draw_slots(&self.input_positions);
|
||||
draw_slots(&self.output_positions);
|
||||
|
||||
for text in self.texts.iter() {
|
||||
painter.add(egui::epaint::TextShape {
|
||||
pos: text.pos,
|
||||
galley: text.galley.to_owned(),
|
||||
underline: Stroke::default(),
|
||||
override_text_color: None,
|
||||
angle: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue