mirror of
https://github.com/eliasstepanik/egui_node_graph.git
synced 2026-01-11 13:58: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