From 8488411b00d20f5d6c343090f7ea70a2591d7b2e Mon Sep 17 00:00:00 2001 From: Tarkin25 Date: Fri, 20 Jan 2023 12:02:29 +0100 Subject: [PATCH] add example for bevy-egui #67 --- Cargo.toml | 1 + egui_node_graph/Cargo.toml | 6 + egui_node_graph/examples/bevy.rs | 67 ++++ egui_node_graph/examples/eframe.rs | 96 +++++ egui_node_graph/examples/node_graph.rs | 525 +++++++++++++++++++++++++ egui_node_graph_example/Cargo.toml | 1 - 6 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 egui_node_graph/examples/bevy.rs create mode 100644 egui_node_graph/examples/eframe.rs create mode 100644 egui_node_graph/examples/node_graph.rs diff --git a/Cargo.toml b/Cargo.toml index d765294..cd94c48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,4 @@ members = [ "egui_node_graph", "egui_node_graph_example", ] +resolver = "2" diff --git a/egui_node_graph/Cargo.toml b/egui_node_graph/Cargo.toml index 25292d4..314bb07 100644 --- a/egui_node_graph/Cargo.toml +++ b/egui_node_graph/Cargo.toml @@ -20,3 +20,9 @@ slotmap = { version = "1.0" } smallvec = { version = "1.7.0" } serde = { version = "1.0", optional = true, features = ["derive"] } thiserror = "1.0" + +[dev-dependencies] +anyhow = "1.0.68" +bevy = "0.9.1" +bevy_egui = "0.17" +eframe = "0.19.0" diff --git a/egui_node_graph/examples/bevy.rs b/egui_node_graph/examples/bevy.rs new file mode 100644 index 0000000..44f335a --- /dev/null +++ b/egui_node_graph/examples/bevy.rs @@ -0,0 +1,67 @@ +mod node_graph; + +use std::collections::HashMap; + +use bevy::prelude::*; +use bevy_egui::{EguiContext, EguiPlugin}; +use egui::TextStyle; +use egui_node_graph::*; +use node_graph::*; + +impl NodeGraphExample { + fn draw(&mut self, ctx: &egui::Context) { + egui::TopBottomPanel::top("top").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + egui::widgets::global_dark_light_mode_switch(ui); + }); + }); + let graph_response = egui::CentralPanel::default() + .show(ctx, |ui| { + self.state + .draw_graph_editor(ui, AllMyNodeTemplates, &mut self.user_state) + }) + .inner; + for node_response in graph_response.node_responses { + // Here, we ignore all other graph events. But you may find + // some use for them. For example, by playing a sound when a new + // connection is created + if let NodeResponse::User(user_event) = node_response { + match user_event { + MyResponse::SetActiveNode(node) => self.user_state.active_node = Some(node), + MyResponse::ClearActiveNode => self.user_state.active_node = None, + } + } + } + + if let Some(node) = self.user_state.active_node { + if self.state.graph.nodes.contains_key(node) { + let text = match evaluate_node(&self.state.graph, node, &mut HashMap::new()) { + Ok(value) => format!("The result is: {:?}", value), + Err(err) => format!("Execution error: {}", err), + }; + ctx.debug_painter().text( + egui::pos2(10.0, 35.0), + egui::Align2::LEFT_TOP, + text, + TextStyle::Button.resolve(&ctx.style()), + egui::Color32::WHITE, + ); + } else { + self.user_state.active_node = None; + } + } + } +} + +fn draw_graph(mut context: ResMut, mut graph: ResMut) { + graph.draw(context.ctx_mut()); +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(EguiPlugin) + .init_resource::() + .add_system(draw_graph) + .run(); +} diff --git a/egui_node_graph/examples/eframe.rs b/egui_node_graph/examples/eframe.rs new file mode 100644 index 0000000..0fb2624 --- /dev/null +++ b/egui_node_graph/examples/eframe.rs @@ -0,0 +1,96 @@ +mod node_graph; + +use std::collections::HashMap; + +use egui::TextStyle; +use egui_node_graph::NodeResponse; +use node_graph::*; + +#[cfg(feature = "persistence")] +const PERSISTENCE_KEY: &str = "egui_node_graph"; + +#[cfg(feature = "persistence")] +impl NodeGraphExample { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let state = cc + .storage + .and_then(|storage| eframe::get_value(storage, PERSISTENCE_KEY)) + .unwrap_or_default(); + + Self { + state, + user_state: MyGraphState::default(), + } + } +} + +impl eframe::App for NodeGraphExample { + #[cfg(feature = "persistence")] + /// If the persistence function is enabled, + /// Called by the frame work to save state before shutdown. + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, PERSISTENCE_KEY, &self.state); + } + /// Called each time the UI needs repainting, which may be many times per second. + /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::TopBottomPanel::top("top").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + egui::widgets::global_dark_light_mode_switch(ui); + }); + }); + let graph_response = egui::CentralPanel::default() + .show(ctx, |ui| { + self.state + .draw_graph_editor(ui, AllMyNodeTemplates, &mut self.user_state) + }) + .inner; + for node_response in graph_response.node_responses { + // Here, we ignore all other graph events. But you may find + // some use for them. For example, by playing a sound when a new + // connection is created + if let NodeResponse::User(user_event) = node_response { + match user_event { + MyResponse::SetActiveNode(node) => self.user_state.active_node = Some(node), + MyResponse::ClearActiveNode => self.user_state.active_node = None, + } + } + } + + if let Some(node) = self.user_state.active_node { + if self.state.graph.nodes.contains_key(node) { + let text = match evaluate_node(&self.state.graph, node, &mut HashMap::new()) { + Ok(value) => format!("The result is: {:?}", value), + Err(err) => format!("Execution error: {}", err), + }; + ctx.debug_painter().text( + egui::pos2(10.0, 35.0), + egui::Align2::LEFT_TOP, + text, + TextStyle::Button.resolve(&ctx.style()), + egui::Color32::WHITE, + ); + } else { + self.user_state.active_node = None; + } + } + } +} + +fn main() { + use eframe::egui::Visuals; + + eframe::run_native( + "Egui node graph example", + eframe::NativeOptions::default(), + Box::new(|cc| { + cc.egui_ctx.set_visuals(Visuals::dark()); + #[cfg(feature = "persistence")] + { + Box::new(NodeGraphExample::new(cc)) + } + #[cfg(not(feature = "persistence"))] + Box::new(NodeGraphExample::default()) + }), + ); +} diff --git a/egui_node_graph/examples/node_graph.rs b/egui_node_graph/examples/node_graph.rs new file mode 100644 index 0000000..86cbe50 --- /dev/null +++ b/egui_node_graph/examples/node_graph.rs @@ -0,0 +1,525 @@ +use std::{borrow::Cow, collections::HashMap}; + +use egui::{self, DragValue}; +use egui_node_graph::*; + +// ========= First, define your user data types ============= + +/// The NodeData holds a custom data struct inside each node. It's useful to +/// store additional information that doesn't live in parameters. For this +/// example, the node data stores the template (i.e. the "type") of the node. +#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] +pub struct MyNodeData { + template: MyNodeTemplate, +} + +/// `DataType`s are what defines the possible range of connections when +/// attaching two ports together. The graph UI will make sure to not allow +/// attaching incompatible datatypes. +#[derive(PartialEq, Eq)] +#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] +pub enum MyDataType { + Scalar, + Vec2, +} + +/// In the graph, input parameters can optionally have a constant value. This +/// value can be directly edited in a widget inside the node itself. +/// +/// There will usually be a correspondence between DataTypes and ValueTypes. But +/// this library makes no attempt to check this consistency. For instance, it is +/// up to the user code in this example to make sure no parameter is created +/// with a DataType of Scalar and a ValueType of Vec2. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] +pub enum MyValueType { + Vec2 { value: egui::Vec2 }, + Scalar { value: f32 }, +} + +impl Default for MyValueType { + fn default() -> Self { + // NOTE: This is just a dummy `Default` implementation. The library + // requires it to circumvent some internal borrow checker issues. + Self::Scalar { value: 0.0 } + } +} + +impl MyValueType { + /// Tries to downcast this value type to a vector + pub fn try_to_vec2(self) -> anyhow::Result { + if let MyValueType::Vec2 { value } = self { + Ok(value) + } else { + anyhow::bail!("Invalid cast from {:?} to vec2", self) + } + } + + /// Tries to downcast this value type to a scalar + pub fn try_to_scalar(self) -> anyhow::Result { + if let MyValueType::Scalar { value } = self { + Ok(value) + } else { + anyhow::bail!("Invalid cast from {:?} to scalar", self) + } + } +} + +/// NodeTemplate is a mechanism to define node templates. It's what the graph +/// will display in the "new node" popup. The user code needs to tell the +/// library how to convert a NodeTemplate into a Node. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] +pub enum MyNodeTemplate { + MakeVector, + MakeScalar, + AddScalar, + SubtractScalar, + VectorTimesScalar, + AddVector, + SubtractVector, +} + +/// The response type is used to encode side-effects produced when drawing a +/// node in the graph. Most side-effects (creating new nodes, deleting existing +/// nodes, handling connections...) are already handled by the library, but this +/// mechanism allows creating additional side effects from user code. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MyResponse { + SetActiveNode(NodeId), + ClearActiveNode, +} + +/// The graph 'global' state. This state struct is passed around to the node and +/// parameter drawing callbacks. The contents of this struct are entirely up to +/// the user. For this example, we use it to keep track of the 'active' node. +#[derive(Default)] +#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))] +pub struct MyGraphState { + pub active_node: Option, +} + +// =========== Then, you need to implement some traits ============ + +// A trait for the data types, to tell the library how to display them +impl DataTypeTrait for MyDataType { + fn data_type_color(&self, _user_state: &mut MyGraphState) -> egui::Color32 { + match self { + MyDataType::Scalar => egui::Color32::from_rgb(38, 109, 211), + MyDataType::Vec2 => egui::Color32::from_rgb(238, 207, 109), + } + } + + fn name(&self) -> Cow<'_, str> { + match self { + MyDataType::Scalar => Cow::Borrowed("scalar"), + MyDataType::Vec2 => Cow::Borrowed("2d vector"), + } + } +} + +// A trait for the node kinds, which tells the library how to build new nodes +// from the templates in the node finder +impl NodeTemplateTrait for MyNodeTemplate { + type NodeData = MyNodeData; + type DataType = MyDataType; + type ValueType = MyValueType; + type UserState = MyGraphState; + + fn node_finder_label(&self, _user_state: &mut Self::UserState) -> Cow<'_, str> { + Cow::Borrowed(match self { + MyNodeTemplate::MakeVector => "New vector", + MyNodeTemplate::MakeScalar => "New scalar", + MyNodeTemplate::AddScalar => "Scalar add", + MyNodeTemplate::SubtractScalar => "Scalar subtract", + MyNodeTemplate::AddVector => "Vector add", + MyNodeTemplate::SubtractVector => "Vector subtract", + MyNodeTemplate::VectorTimesScalar => "Vector times scalar", + }) + } + + fn node_graph_label(&self, user_state: &mut Self::UserState) -> String { + // It's okay to delegate this to node_finder_label if you don't want to + // show different names in the node finder and the node itself. + self.node_finder_label(user_state).into() + } + + fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData { + MyNodeData { template: *self } + } + + fn build_node( + &self, + graph: &mut Graph, + _user_state: &mut Self::UserState, + node_id: NodeId, + ) { + // The nodes are created empty by default. This function needs to take + // care of creating the desired inputs and outputs based on the template + + // We define some closures here to avoid boilerplate. Note that this is + // entirely optional. + let input_scalar = |graph: &mut MyGraph, name: &str| { + graph.add_input_param( + node_id, + name.to_string(), + MyDataType::Scalar, + MyValueType::Scalar { value: 0.0 }, + InputParamKind::ConnectionOrConstant, + true, + ); + }; + let input_vector = |graph: &mut MyGraph, name: &str| { + graph.add_input_param( + node_id, + name.to_string(), + MyDataType::Vec2, + MyValueType::Vec2 { + value: egui::vec2(0.0, 0.0), + }, + InputParamKind::ConnectionOrConstant, + true, + ); + }; + + let output_scalar = |graph: &mut MyGraph, name: &str| { + graph.add_output_param(node_id, name.to_string(), MyDataType::Scalar); + }; + let output_vector = |graph: &mut MyGraph, name: &str| { + graph.add_output_param(node_id, name.to_string(), MyDataType::Vec2); + }; + + match self { + MyNodeTemplate::AddScalar => { + // The first input param doesn't use the closure so we can comment + // it in more detail. + graph.add_input_param( + node_id, + // This is the name of the parameter. Can be later used to + // retrieve the value. Parameter names should be unique. + "A".into(), + // The data type for this input. In this case, a scalar + MyDataType::Scalar, + // The value type for this input. We store zero as default + MyValueType::Scalar { value: 0.0 }, + // The input parameter kind. This allows defining whether a + // parameter accepts input connections and/or an inline + // widget to set its value. + InputParamKind::ConnectionOrConstant, + true, + ); + input_scalar(graph, "B"); + output_scalar(graph, "out"); + } + MyNodeTemplate::SubtractScalar => { + input_scalar(graph, "A"); + input_scalar(graph, "B"); + output_scalar(graph, "out"); + } + MyNodeTemplate::VectorTimesScalar => { + input_scalar(graph, "scalar"); + input_vector(graph, "vector"); + output_vector(graph, "out"); + } + MyNodeTemplate::AddVector => { + input_vector(graph, "v1"); + input_vector(graph, "v2"); + output_vector(graph, "out"); + } + MyNodeTemplate::SubtractVector => { + input_vector(graph, "v1"); + input_vector(graph, "v2"); + output_vector(graph, "out"); + } + MyNodeTemplate::MakeVector => { + input_scalar(graph, "x"); + input_scalar(graph, "y"); + output_vector(graph, "out"); + } + MyNodeTemplate::MakeScalar => { + input_scalar(graph, "value"); + output_scalar(graph, "out"); + } + } + } +} + +pub struct AllMyNodeTemplates; +impl NodeTemplateIter for AllMyNodeTemplates { + type Item = MyNodeTemplate; + + fn all_kinds(&self) -> Vec { + // This function must return a list of node kinds, which the node finder + // will use to display it to the user. Crates like strum can reduce the + // boilerplate in enumerating all variants of an enum. + vec![ + MyNodeTemplate::MakeScalar, + MyNodeTemplate::MakeVector, + MyNodeTemplate::AddScalar, + MyNodeTemplate::SubtractScalar, + MyNodeTemplate::AddVector, + MyNodeTemplate::SubtractVector, + MyNodeTemplate::VectorTimesScalar, + ] + } +} + +impl WidgetValueTrait for MyValueType { + type Response = MyResponse; + type UserState = MyGraphState; + type NodeData = MyNodeData; + fn value_widget( + &mut self, + param_name: &str, + _node_id: NodeId, + ui: &mut egui::Ui, + _user_state: &mut MyGraphState, + _node_data: &MyNodeData, + ) -> Vec { + // This trait is used to tell the library which UI to display for the + // inline parameter widgets. + match self { + MyValueType::Vec2 { value } => { + ui.label(param_name); + ui.horizontal(|ui| { + ui.label("x"); + ui.add(DragValue::new(&mut value.x)); + ui.label("y"); + ui.add(DragValue::new(&mut value.y)); + }); + } + MyValueType::Scalar { value } => { + ui.horizontal(|ui| { + ui.label(param_name); + ui.add(DragValue::new(value)); + }); + } + } + // This allows you to return your responses from the inline widgets. + Vec::new() + } +} + +impl UserResponseTrait for MyResponse {} +impl NodeDataTrait for MyNodeData { + type Response = MyResponse; + type UserState = MyGraphState; + type DataType = MyDataType; + type ValueType = MyValueType; + + // This method will be called when drawing each node. This allows adding + // extra ui elements inside the nodes. In this case, we create an "active" + // button which introduces the concept of having an active node in the + // graph. This is done entirely from user code with no modifications to the + // node graph library. + fn bottom_ui( + &self, + ui: &mut egui::Ui, + node_id: NodeId, + _graph: &Graph, + user_state: &mut Self::UserState, + ) -> Vec> + where + MyResponse: UserResponseTrait, + { + // This logic is entirely up to the user. In this case, we check if the + // current node we're drawing is the active one, by comparing against + // the value stored in the global user state, and draw different button + // UIs based on that. + + let mut responses = vec![]; + let is_active = user_state + .active_node + .map(|id| id == node_id) + .unwrap_or(false); + + // Pressing the button will emit a custom user response to either set, + // or clear the active node. These responses do nothing by themselves, + // the library only makes the responses available to you after the graph + // has been drawn. See below at the update method for an example. + if !is_active { + if ui.button("👁 Set active").clicked() { + responses.push(NodeResponse::User(MyResponse::SetActiveNode(node_id))); + } + } else { + let button = + egui::Button::new(egui::RichText::new("👁 Active").color(egui::Color32::BLACK)) + .fill(egui::Color32::GOLD); + if ui.add(button).clicked() { + responses.push(NodeResponse::User(MyResponse::ClearActiveNode)); + } + } + + responses + } +} + +type MyGraph = Graph; +type MyEditorState = + GraphEditorState; + +#[derive(Default, bevy::prelude::Resource)] +pub struct NodeGraphExample { + // The `GraphEditorState` is the top-level object. You "register" all your + // custom types by specifying it as its generic parameters. + pub state: MyEditorState, + + pub user_state: MyGraphState, +} + +type OutputsCache = HashMap; + +/// Recursively evaluates all dependencies of this node, then evaluates the node itself. +pub fn evaluate_node( + graph: &MyGraph, + node_id: NodeId, + outputs_cache: &mut OutputsCache, +) -> anyhow::Result { + // To solve a similar problem as creating node types above, we define an + // Evaluator as a convenience. It may be overkill for this small example, + // but something like this makes the code much more readable when the + // number of nodes starts growing. + + struct Evaluator<'a> { + graph: &'a MyGraph, + outputs_cache: &'a mut OutputsCache, + node_id: NodeId, + } + impl<'a> Evaluator<'a> { + fn new(graph: &'a MyGraph, outputs_cache: &'a mut OutputsCache, node_id: NodeId) -> Self { + Self { + graph, + outputs_cache, + node_id, + } + } + fn evaluate_input(&mut self, name: &str) -> anyhow::Result { + // Calling `evaluate_input` recursively evaluates other nodes in the + // graph until the input value for a paramater has been computed. + evaluate_input(self.graph, self.node_id, name, self.outputs_cache) + } + fn populate_output( + &mut self, + name: &str, + value: MyValueType, + ) -> anyhow::Result { + // After computing an output, we don't just return it, but we also + // populate the outputs cache with it. This ensures the evaluation + // only ever computes an output once. + // + // The return value of the function is the "final" output of the + // node, the thing we want to get from the evaluation. The example + // would be slightly more contrived when we had multiple output + // values, as we would need to choose which of the outputs is the + // one we want to return. Other outputs could be used as + // intermediate values. + // + // Note that this is just one possible semantic interpretation of + // the graphs, you can come up with your own evaluation semantics! + populate_output(self.graph, self.outputs_cache, self.node_id, name, value) + } + fn input_vector(&mut self, name: &str) -> anyhow::Result { + self.evaluate_input(name)?.try_to_vec2() + } + fn input_scalar(&mut self, name: &str) -> anyhow::Result { + self.evaluate_input(name)?.try_to_scalar() + } + fn output_vector(&mut self, name: &str, value: egui::Vec2) -> anyhow::Result { + self.populate_output(name, MyValueType::Vec2 { value }) + } + fn output_scalar(&mut self, name: &str, value: f32) -> anyhow::Result { + self.populate_output(name, MyValueType::Scalar { value }) + } + } + + let node = &graph[node_id]; + let mut evaluator = Evaluator::new(graph, outputs_cache, node_id); + match node.user_data.template { + MyNodeTemplate::AddScalar => { + let a = evaluator.input_scalar("A")?; + let b = evaluator.input_scalar("B")?; + evaluator.output_scalar("out", a + b) + } + MyNodeTemplate::SubtractScalar => { + let a = evaluator.input_scalar("A")?; + let b = evaluator.input_scalar("B")?; + evaluator.output_scalar("out", a - b) + } + MyNodeTemplate::VectorTimesScalar => { + let scalar = evaluator.input_scalar("scalar")?; + let vector = evaluator.input_vector("vector")?; + evaluator.output_vector("out", vector * scalar) + } + MyNodeTemplate::AddVector => { + let v1 = evaluator.input_vector("v1")?; + let v2 = evaluator.input_vector("v2")?; + evaluator.output_vector("out", v1 + v2) + } + MyNodeTemplate::SubtractVector => { + let v1 = evaluator.input_vector("v1")?; + let v2 = evaluator.input_vector("v2")?; + evaluator.output_vector("out", v1 - v2) + } + MyNodeTemplate::MakeVector => { + let x = evaluator.input_scalar("x")?; + let y = evaluator.input_scalar("y")?; + evaluator.output_vector("out", egui::vec2(x, y)) + } + MyNodeTemplate::MakeScalar => { + let value = evaluator.input_scalar("value")?; + evaluator.output_scalar("out", value) + } + } +} + +fn populate_output( + graph: &MyGraph, + outputs_cache: &mut OutputsCache, + node_id: NodeId, + param_name: &str, + value: MyValueType, +) -> anyhow::Result { + let output_id = graph[node_id].get_output(param_name)?; + outputs_cache.insert(output_id, value); + Ok(value) +} + +// Evaluates the input value of +fn evaluate_input( + graph: &MyGraph, + node_id: NodeId, + param_name: &str, + outputs_cache: &mut OutputsCache, +) -> anyhow::Result { + let input_id = graph[node_id].get_input(param_name)?; + + // The output of another node is connected. + if let Some(other_output_id) = graph.connection(input_id) { + // The value was already computed due to the evaluation of some other + // node. We simply return value from the cache. + if let Some(other_value) = outputs_cache.get(&other_output_id) { + Ok(*other_value) + } + // This is the first time encountering this node, so we need to + // recursively evaluate it. + else { + // Calling this will populate the cache + evaluate_node(graph, graph[other_output_id].node, outputs_cache)?; + + // Now that we know the value is cached, return it + Ok(*outputs_cache + .get(&other_output_id) + .expect("Cache should be populated")) + } + } + // No existing connection, take the inline value instead. + else { + Ok(graph[input_id].value) + } +} + +/// Files in the 'examples' directory need to have a main function, but this file is only meant to be included in framework-specific examples like 'eframe' or 'bevy' +#[allow(dead_code)] +fn main() { + panic!("This is not an example"); +} diff --git a/egui_node_graph_example/Cargo.toml b/egui_node_graph_example/Cargo.toml index 7748d93..49fe6fc 100644 --- a/egui_node_graph_example/Cargo.toml +++ b/egui_node_graph_example/Cargo.toml @@ -3,7 +3,6 @@ name = "egui_node_graph_example" version = "0.1.0" authors = ["setzer22"] edition = "2021" -rust-version = "1.56" [lib] crate-type = ["cdylib", "rlib"]