Initial commit

This commit is contained in:
setzer22 2022-02-19 10:19:31 +01:00
commit d8b81a09db
9 changed files with 456 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

20
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
}