mirror of
https://github.com/eliasstepanik/egui_node_graph.git
synced 2026-01-14 15:28:28 +00:00
Initial commit
This commit is contained in:
commit
d8b81a09db
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "egui_node_graph"
|
||||||
|
description = "A helper library to create interactive node graphs using egui"
|
||||||
|
homepage = "https://github.com/setzer22/egui_node_graph"
|
||||||
|
repository = "https://github.com/setzer22/egui_node_graph"
|
||||||
|
license = "MIT"
|
||||||
|
version = "0.1.0"
|
||||||
|
keywords = ["ui", "egui", "graph", "node"]
|
||||||
|
authors = ["setzer22"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
persistence = ["serde", "slotmap/serde", "smallvec/serde"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = { version = "0.16" }
|
||||||
|
slotmap = { version = "1.0" }
|
||||||
|
smallvec = { version = "1.7.0" }
|
||||||
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
thiserror = "1.0"
|
||||||
10
src/error.rs
Normal file
10
src/error.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum EguiGraphError {
|
||||||
|
#[error("Node {0:?} has no parameter named {1}")]
|
||||||
|
NoParameterNamed(NodeId, String),
|
||||||
|
|
||||||
|
#[error("Parameter {0:?} was not found in the graph.")]
|
||||||
|
InvalidParameterId(AnyParameterId)
|
||||||
|
}
|
||||||
130
src/graph_impls.rs
Normal file
130
src/graph_impls.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl<NodeData, DataType, ValueType> Graph<NodeData, DataType, ValueType> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nodes: SlotMap::default(),
|
||||||
|
inputs: SlotMap::default(),
|
||||||
|
outputs: SlotMap::default(),
|
||||||
|
connections: SecondaryMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node(
|
||||||
|
&mut self,
|
||||||
|
label: String,
|
||||||
|
user_data: NodeData,
|
||||||
|
f: impl FnOnce(NodeId, &mut Node<NodeData>),
|
||||||
|
) -> NodeId {
|
||||||
|
let node_id = self.nodes.insert_with_key(|node_id| {
|
||||||
|
Node {
|
||||||
|
id: node_id,
|
||||||
|
label,
|
||||||
|
// These get filled in later by the user function
|
||||||
|
inputs: Vec::default(),
|
||||||
|
outputs: Vec::default(),
|
||||||
|
user_data,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
f(node_id, self.nodes.get_mut(node_id).unwrap());
|
||||||
|
|
||||||
|
node_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_node(&mut self, node_id: NodeId) {
|
||||||
|
self.connections
|
||||||
|
.retain(|i, o| !(self.outputs[*o].node == node_id || self.inputs[i].node == node_id));
|
||||||
|
let inputs: SVec<_> = self[node_id].input_ids().collect();
|
||||||
|
for input in inputs {
|
||||||
|
self.inputs.remove(input);
|
||||||
|
}
|
||||||
|
let outputs: SVec<_> = self[node_id].output_ids().collect();
|
||||||
|
for output in outputs {
|
||||||
|
self.outputs.remove(output);
|
||||||
|
}
|
||||||
|
self.nodes.remove(node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_connection(&mut self, input_id: InputId) -> Option<OutputId> {
|
||||||
|
self.connections.remove(input_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||||
|
self.nodes.iter().map(|(id, _)| id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_connection(&mut self, output: OutputId, input: InputId) {
|
||||||
|
self.connections.insert(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_connections(&self) -> impl Iterator<Item = (InputId, OutputId)> + '_ {
|
||||||
|
self.connections.iter().map(|(o, i)| (o, *i))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection(&self, input: InputId) -> Option<OutputId> {
|
||||||
|
self.connections.get(input).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn any_param_type(&self, param: AnyParameterId) -> Result<&DataType, EguiGraphError> {
|
||||||
|
match param {
|
||||||
|
AnyParameterId::Input(input) => self.inputs.get(input).map(|x| &x.typ),
|
||||||
|
AnyParameterId::Output(output) => self.outputs.get(output).map(|x| &x.typ),
|
||||||
|
}
|
||||||
|
.ok_or(EguiGraphError::InvalidParameterId(param))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_input(&self, input: InputId) -> &InputParam<DataType, ValueType> {
|
||||||
|
&self.inputs[input]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_output(&self, output: OutputId) -> &OutputParam<DataType> {
|
||||||
|
&self.outputs[output]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NodeData, DataType, ValueType> Default for Graph<NodeData, DataType, ValueType> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NodeData> Node<NodeData> {
|
||||||
|
pub fn inputs<'a, DataType, DataValue>(
|
||||||
|
&'a self,
|
||||||
|
graph: &'a Graph<NodeData, DataType, DataValue>,
|
||||||
|
) -> impl Iterator<Item = &InputParam<DataType, DataValue>> + 'a {
|
||||||
|
self.input_ids().map(|id| graph.get_input(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn outputs<'a, DataType, DataValue>(
|
||||||
|
&'a self,
|
||||||
|
graph: &'a Graph<NodeData, DataType, DataValue>,
|
||||||
|
) -> impl Iterator<Item = &OutputParam<DataType>> + 'a {
|
||||||
|
self.output_ids().map(|id| graph.get_output(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input_ids(&self) -> impl Iterator<Item = InputId> + '_ {
|
||||||
|
self.inputs.iter().map(|(_name, id)| *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_ids(&self) -> impl Iterator<Item = OutputId> + '_ {
|
||||||
|
self.outputs.iter().map(|(_name, id)| *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_input(&self, name: &str) -> Result<InputId, EguiGraphError> {
|
||||||
|
self.inputs
|
||||||
|
.iter()
|
||||||
|
.find(|(param_name, _id)| param_name == name)
|
||||||
|
.map(|x| x.1)
|
||||||
|
.ok_or_else(|| EguiGraphError::NoParameterNamed(self.id, name.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_output(&self, name: &str) -> Result<OutputId, EguiGraphError> {
|
||||||
|
self.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|(param_name, _id)| param_name == name)
|
||||||
|
.map(|x| x.1)
|
||||||
|
.ok_or_else(|| EguiGraphError::NoParameterNamed(self.id, name.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/id_type.rs
Normal file
24
src/id_type.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
slotmap::new_key_type! { pub struct NodeId; }
|
||||||
|
slotmap::new_key_type! { pub struct InputId; }
|
||||||
|
slotmap::new_key_type! { pub struct OutputId; }
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum AnyParameterId {
|
||||||
|
Input(InputId),
|
||||||
|
Output(OutputId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyParameterId {
|
||||||
|
pub fn assume_input(&self) -> InputId {
|
||||||
|
match self {
|
||||||
|
AnyParameterId::Input(input) => *input,
|
||||||
|
AnyParameterId::Output(output) => panic!("{:?} is not an InputId", output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn assume_output(&self) -> OutputId {
|
||||||
|
match self {
|
||||||
|
AnyParameterId::Output(output) => *output,
|
||||||
|
AnyParameterId::Input(input) => panic!("{:?} is not an OutputId", input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/index_impls.rs
Normal file
35
src/index_impls.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! impl_index_traits {
|
||||||
|
($id_type:ty, $output_type:ty, $arena:ident) => {
|
||||||
|
impl<A, B, C> std::ops::Index<$id_type> for Graph<A, B, C> {
|
||||||
|
type Output = $output_type;
|
||||||
|
|
||||||
|
fn index(&self, index: $id_type) -> &Self::Output {
|
||||||
|
self.$arena.get(index).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{} index error for {:?}. Has the value been deleted?",
|
||||||
|
stringify!($id_type),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, B, C> std::ops::IndexMut<$id_type> for Graph<A, B, C> {
|
||||||
|
fn index_mut(&mut self, index: $id_type) -> &mut Self::Output {
|
||||||
|
self.$arena.get_mut(index).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{} index error for {:?}. Has the value been deleted?",
|
||||||
|
stringify!($id_type),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_index_traits!(NodeId, Node<A>, nodes);
|
||||||
|
impl_index_traits!(InputId, InputParam<B, C>, inputs);
|
||||||
|
impl_index_traits!(OutputId, OutputParam<B>, outputs);
|
||||||
89
src/lib.rs
Normal file
89
src/lib.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
use slotmap::{SecondaryMap, SlotMap};
|
||||||
|
|
||||||
|
pub mod id_type;
|
||||||
|
pub use id_type::*;
|
||||||
|
|
||||||
|
pub mod index_impls;
|
||||||
|
|
||||||
|
pub mod graph_impls;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub use error::*;
|
||||||
|
|
||||||
|
pub mod ui_state;
|
||||||
|
pub use ui_state::*;
|
||||||
|
|
||||||
|
pub mod node_finder;
|
||||||
|
pub use node_finder::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "persistence")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub type SVec<T> = smallvec::SmallVec<[T; 4]>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub struct Node<NodeData> {
|
||||||
|
pub id: NodeId,
|
||||||
|
pub label: String,
|
||||||
|
pub inputs: Vec<(String, InputId)>,
|
||||||
|
pub outputs: Vec<(String, OutputId)>,
|
||||||
|
pub user_data: NodeData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// There are three kinds of input params
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub enum InputParamKind {
|
||||||
|
/// No constant value can be set. Only incoming connections can produce it
|
||||||
|
ConnectionOnly,
|
||||||
|
/// Only a constant value can be set. No incoming connections accepted.
|
||||||
|
ConstantOnly,
|
||||||
|
/// Both incoming connections and constants are accepted. Connections take
|
||||||
|
/// precedence over the constant values.
|
||||||
|
ConnectionOrConstant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "persistence")]
|
||||||
|
fn shown_inline_default() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub struct InputParam<DataType, ValueType> {
|
||||||
|
pub id: InputId,
|
||||||
|
/// The data type of this node. Used to determine incoming connections. This
|
||||||
|
/// should always match the type of the InputParamValue, but the property is
|
||||||
|
/// not actually enforced.
|
||||||
|
pub typ: DataType,
|
||||||
|
/// The constant value stored in this parameter.
|
||||||
|
pub value: ValueType,
|
||||||
|
/// The input kind. See [`InputParamKind`]
|
||||||
|
pub kind: InputParamKind,
|
||||||
|
/// Back-reference to the node containing this parameter.
|
||||||
|
pub node: NodeId,
|
||||||
|
/// When true, the node is shown inline inside the node graph.
|
||||||
|
#[cfg_attr(feature = "persistence", serde(default = "shown_inline_default"))]
|
||||||
|
pub shown_inline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub struct OutputParam<DataType> {
|
||||||
|
pub id: OutputId,
|
||||||
|
/// Back-reference to the node containing this parameter.
|
||||||
|
pub node: NodeId,
|
||||||
|
pub typ: DataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub struct Graph<NodeData, DataType, ValueType> {
|
||||||
|
pub nodes: SlotMap<NodeId, Node<NodeData>>,
|
||||||
|
pub inputs: SlotMap<InputId, InputParam<DataType, ValueType>>,
|
||||||
|
pub outputs: SlotMap<OutputId, OutputParam<DataType>>,
|
||||||
|
// Connects the input of a node, to the output of its predecessor that
|
||||||
|
// produces it
|
||||||
|
connections: SecondaryMap<InputId, OutputId>,
|
||||||
|
}
|
||||||
75
src/node_finder.rs
Normal file
75
src/node_finder.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NodeFinder<NodeKind> {
|
||||||
|
query: String,
|
||||||
|
/// Reset every frame. When set, the node finder will be moved at that position
|
||||||
|
pub position: Option<Pos2>,
|
||||||
|
pub just_spawned: bool,
|
||||||
|
_phantom: PhantomData<NodeKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NodeKindIter {
|
||||||
|
type Item;
|
||||||
|
fn all_kinds<'a>() -> Box<dyn Iterator<Item=&'a Self::Item>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NodeKind> NodeFinder<NodeKind> {
|
||||||
|
pub fn new_at(pos: Pos2) -> Self {
|
||||||
|
NodeFinder {
|
||||||
|
position: Some(pos),
|
||||||
|
just_spawned: true,
|
||||||
|
_phantom: Default::default(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the node selector panel with a search bar. Returns whether a node
|
||||||
|
/// archetype was selected and, in that case, the finder should be hidden on
|
||||||
|
/// the next frame.
|
||||||
|
pub fn show(&mut self, ui: &mut Ui, all_kinds: impl NodeKindIter<Item=NodeKind>) -> Option<NodeKind> {
|
||||||
|
let background_color = color_from_hex("#3f3f3f").unwrap();
|
||||||
|
let _titlebar_color = background_color.linear_multiply(0.8);
|
||||||
|
let text_color = color_from_hex("#fefefe").unwrap();
|
||||||
|
|
||||||
|
ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.0, text_color);
|
||||||
|
|
||||||
|
let frame = Frame::dark_canvas(ui.style())
|
||||||
|
.fill(background_color)
|
||||||
|
.margin(vec2(5.0, 5.0));
|
||||||
|
|
||||||
|
// The archetype that will be returned.
|
||||||
|
let mut submitted_archetype = None;
|
||||||
|
frame.show(ui, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let resp = ui.text_edit_singleline(&mut self.query);
|
||||||
|
if self.just_spawned {
|
||||||
|
resp.request_focus();
|
||||||
|
self.just_spawned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query_submit = resp.lost_focus() && ui.input().key_down(Key::Enter);
|
||||||
|
|
||||||
|
Frame::default().margin(vec2(10.0, 10.0)).show(ui, |ui| {
|
||||||
|
for archetype in all_kinds.all_kinds() {
|
||||||
|
let archetype_name = archetype.type_label();
|
||||||
|
if archetype_name.contains(self.query.as_str()) {
|
||||||
|
if query_submit {
|
||||||
|
submitted_archetype = Some(archetype);
|
||||||
|
query_submit = false;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(false, archetype_name).clicked() {
|
||||||
|
submitted_archetype = Some(archetype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
submitted_archetype
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/ui_state.rs
Normal file
71
src/ui_state.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "persistence")]
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(Serialize, Deserialize))]
|
||||||
|
pub struct PanZoom {
|
||||||
|
pub pan: egui::Vec2,
|
||||||
|
pub zoom: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphEditorState<NodeData, DataType, ValueType> {
|
||||||
|
pub graph: Graph<NodeData, DataType, ValueType>,
|
||||||
|
/// Nodes are drawn in this order. Draw order is important because nodes
|
||||||
|
/// that are drawn last are on top.
|
||||||
|
pub node_order: Vec<NodeId>,
|
||||||
|
/// An ongoing connection interaction: The mouse has dragged away from a
|
||||||
|
/// port and the user is holding the click
|
||||||
|
pub connection_in_progress: Option<(NodeId, AnyParameterId)>,
|
||||||
|
/// The currently active node. A program will be compiled to compute the
|
||||||
|
/// result of this node and constantly updated in real-time.
|
||||||
|
pub active_node: Option<NodeId>,
|
||||||
|
/// The currently selected node. Some interface actions depend on the
|
||||||
|
/// currently selected node.
|
||||||
|
pub selected_node: Option<NodeId>,
|
||||||
|
/// The position of each node.
|
||||||
|
pub node_positions: SecondaryMap<NodeId, egui::Pos2>,
|
||||||
|
/// The node finder is used to create new nodes.
|
||||||
|
pub node_finder: Option<NodeFinder>,
|
||||||
|
/// When this option is set by the UI, the side effect encoded by the node
|
||||||
|
/// will be executed at the start of the next frame.
|
||||||
|
pub run_side_effect: Option<NodeId>,
|
||||||
|
/// The panning of the graph viewport.
|
||||||
|
pub pan_zoom: PanZoom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NodeData, DataType, ValueType> GraphEditorState<NodeData, DataType, ValueType> {
|
||||||
|
pub fn new(default_zoom: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
graph: Graph::new(),
|
||||||
|
node_order: Vec::new(),
|
||||||
|
connection_in_progress: None,
|
||||||
|
active_node: None,
|
||||||
|
selected_node: None,
|
||||||
|
run_side_effect: None,
|
||||||
|
node_positions: SecondaryMap::new(),
|
||||||
|
node_finder: None,
|
||||||
|
pan_zoom: PanZoom {
|
||||||
|
pan: egui::Vec2::ZERO,
|
||||||
|
zoom: default_zoom,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanZoom {
|
||||||
|
pub fn adjust_zoom(
|
||||||
|
&mut self,
|
||||||
|
zoom_delta: f32,
|
||||||
|
point: egui::Vec2,
|
||||||
|
zoom_min: f32,
|
||||||
|
zoom_max: f32,
|
||||||
|
) {
|
||||||
|
let zoom_clamped = (self.zoom + zoom_delta).clamp(zoom_min, zoom_max);
|
||||||
|
let zoom_delta = zoom_clamped - self.zoom;
|
||||||
|
|
||||||
|
self.zoom += zoom_delta;
|
||||||
|
self.pan += point * zoom_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user