setzer22 / egui_node_graph Goto Github PK
View Code? Open in Web Editor NEWBuild your node graph applications in Rust, using egui
License: MIT License
Build your node graph applications in Rust, using egui
License: MIT License
It would be helpful to know how to use the "persistence" feature. I presume it uses serde to store state to persistence storage, but how to use it?
I was just messing around with your example and it looks really cool! For my intended use case I need to keep some external data structure up to date with your graph editor. For this use case a "CreatedNode(NodeId)" NodeResponse would be beneficial. Just tried to clone the repo and add it myself, and that was really simple :)
I need to track the changes to the UI graph and apply them to some data model. However, the input and output IDs of the connections don't tell me much, I really need to know the indices within the connections (as in, was it connected to the 2nd input or the 1st output, etc.). I think the only way I can look those up (apart from doing separate bookkeeping) is to query the nodes in question and then look up the id in their list of inputs and outputs, respectively.
But then I run into a problem with deleted nodes, because by the time you're handling the NodeResponse
, the node and their input and outputparams are already removed from the graph.
I did some digging through the source code, and it seems the order of events is as follows:
DeleteNodeUi
<potentially some other events>
any number of DisconnectEvent
s (generated by DeleteNodeUi
)
DeleteFullNode
(generated by DeleteNodeUi
)
Now, the DeleteFullNode
contains the original Node
, but I really like to know that during the DisconnectEvent
s caused by the removal of that node.
Of course there are some ways around this. I already mentioned separate bookkeeping, but one could also query whether the node exists during response handling, find out that it doesn't, and then queue up those events so you can process them when the the DeleteFullNode
for the respective node comes around.
I don't see a good solution from the API side though. Perhaps some shadow storage that keeps the data around until the next update? In any way, it might be a good idea to mention in the comments that the IDs as reported by the events might be stale.
Changes in #19 introduced a few new edge cases that need to be accounted for:
None of this affects the original mode of operation, where the graph was being drawn as a full-screen CentralPanel.
Hey there! I'm building off the example to work for my use case, and I noticed you're using macros to construct the node parameters:
macro_rules! input {
(scalar $name:expr) => {
graph.add_input_param(
node_id,
$name.to_string(),
MyDataType::Scalar,
MyValueType::Scalar { value: 0.0 },
InputParamKind::ConnectionOrConstant,
true,
);
};
// ...
}
macro_rules! output {
(scalar $name:expr) => {
graph.add_output_param(node_id, $name.to_string(), MyDataType::Scalar);
};
// ...
}
// ...
match self {
MyNodeTemplate::SubtractScalar => {
input!(scalar "A");
input!(scalar "B");
output!(scalar "out");
}
}
I'd suggest using lambdas, because they're easier to parameterise and (in my opinion) cleaner:
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 output_scalar = |graph: &mut MyGraph, name: &str| {
graph.add_output_param(node_id, name.to_string(), MyDataType::Scalar);
};
match self {
MyNodeTemplate::SubtractScalar => {
input_scalar(graph, "A");
input_scalar(graph, "B");
output_scalar(graph, "out");
}
}
In #40, a change was introduced where the check to spawn the node finder used mouse.button_released
instead of mouse.secondary_down()
. This is the more correct behavior, as you only want to spawn the finder once the click is complete. Unfortunately, this is only available on egui master, not 0.18, so this change cannot be applied until egui 0.19 is released.
I'm leaving this issue as a reminder
The reason I am asking : After playing and extending a bit the provided egui_node_graph_example
I started to dive into the next stage: Adapting a minimal version of the blackjack PolyAsm
based graph compiler, so I can do node traversal and finally get some executable instructions. Unfortunately this is a bit outside my comfort zone and I ran into an error about wrong lifetimes and at several places I was not comfortable what I was doing to get things to compile and I was also in doubt how to adapt blackjack NodeData to egui_node_graph Node<NodeData>.user_data
.
Anyway, if you plan to make blackjack use egui_node_graph
, then I probably could figure out myself how to get my minimal compiler example to work, otherwise a minimal working example of something like a graph compiler would be great.
Originally posted by rsaccon February 25, 2022
I want to find all final nodes, which is all nodes which do not have out-going connection. I haven't found any in-built way at egui_node_graph to do this.
What I found is a way to find a connection based on the InputId
, by using:
egui_node_graph::graph_impls::Graph
pub fn connection(&self, input: InputId) -> Option<OutputId>
But with that approach, to find out for one specific node, whether it has no outgoing connections, I would have to iterate over all inputs of all nodes to query if they have a connection to that specific node, which is not very efficient. Is there a better way to do this ?
If there is no good way to do this yet, I suggest we add that capability to egui_node_graph by maintaining a list of final nodes (which needs to be adapted each time a connection gets added or removed).
Right now, the node graph can always be edited by the user. I think a read-only mode could be a nice feature to have. I could for example allow nodes to be moved around while preventing the user from editing connections or removing a node.
A use case for this functionality could involve enabling users to modify the values within the nodes of a generated computation flow.
I feel like it could be related to #6 to some extent.
For now as I understand the example is only for displaying the nodes visually. Would nice to have also demonstrated how to connect the functional part. Like doing the adding, substraction, etc.
Having a compile-time list of possible node types can be limiting for some use cases, because one might need "dynamic types" of nodes.
A few use cases that come to my mind:
1- I might want to implement the possibility for my user to select a bunch of nodes, group them, and create a "new template" that, to my application logic, is equivalent of all the grouped nodes.
2- I might want to have plugins for my application, and the plugin would register new node types.
Since any node always contains the user-provided NodeData generic, the application can use that struct/enum to differentiate between all the possible operations that a node represents.
Im facing this error when i try to compile the node editor to web:
error[E0061]: this function takes 3 arguments but 2 arguments were supplied
--> egui_node_graph_example\src\lib.rs:22:5
|
22 | eframe::start_web(canvas_id, Box::new(app))
| ^^^^^^^^^^^^^^^^^-------------------------- an argument of type Box<(dyn for<'r, 's> FnOnce(&'r CreationContext<'s>) -> Box<(dyn App + 'static)> + 'static)>
is missing
|
note: expected struct WebOptions
, found struct Box
--> egui_node_graph_example\src\lib.rs:22:34
|
22 | eframe::start_web(canvas_id, Box::new(app))
| ^^^^^^^^^^^^^
= note: expected struct WebOptions
found struct Box<NodeGraphExample>
note: function defined here
--> C:\Users\2021.cargo\registry\src\github.com-1ecc6299db9ec823\eframe-0.19.0\src\lib.rs:112:8
|
112 | pub fn start_web(
| ^^^^^^^^^
help: provide the argument
|
22 | eframe::start_web(canvas_id, /* WebOptions /, / Box<(dyn for<'r, 's> FnOnce(&'r CreationContext<'s>) -> Box<(dyn App + 'static)> + 'static)> */)
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error[E0308]: mismatched types
--> egui_node_graph_example\src\lib.rs:22:5
|
20 | pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
| ----------------------------------------- expected Result<(), JsValue>
because of return type
21 | let app = NodeGraphExample::default();
22 | eframe::start_web(canvas_id, Box::new(app))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected ()
, found struct Arc
|
= note: expected enum Result<(), _>
found enum Result<Arc<eframe::egui::mutex::Mutex<AppRunner>>, _>
Some errors have detailed explanations: E0061, E0308.
For more information about an error, try rustc --explain E0061
.
error: could not compile egui_node_graph_example
due to 2 previous errors
In some cases, you do not want to implement serde in UserState but want to pass it as an argument to bottom_ui().
However, currently, if the user implements serde in GraphEditorState, the user must also implement it in UserState.
Therefore, we suggest not keeping UserState in GraphEditorState.
Like this,
draw_graph_editor(ui, AllMyNodeTemplates, &self.user_state)
Now, if change to light mode :
This makes the right-click popup text unreadable.
So I made the following modifications to accommodate light mode.
I would like to hear your opinions on the colors.
This modified branch is kkngsm/egui_node_graph@fffdaeb
Hello, first of all thank you for extracting this amazing library from blackjack
and making it available!
I was wondering if I might have missed the support for zooming in and out of the node graph, like possible in blackjack?
I see panning being implemented by default by pressing middle mouse button and moving, but I don't see any handling for the scroll wheel in the code.
When changing the scroll
value of the PanZoom
struct manually, there does not seem to be any visual feedback of a change. Is zooming supported as of now and if not, can it be ported over from blackjack, too?
I could also work an a PR to port this functionality, but since I don't know the blackjack code well I would not know where to start.
This crate expects epaint::color::Color32
as return value from DataTypeTrait::data_type_color()
, but Color32
is at epaint::Color32
.
Can you please publish a bugfix release for fixing this?
Right now when the user right-clicks on the graph editor, the "add new node" popup appears. I think this is something that should be done in application code, instead of library code. I can see a few situations in which a programmer would want to have a specific context menu:
1- When clicking on a node, I might want a menu that allows me to delete, rename, or clone it, or do other fancy stuff.
2- When clicking on the background, I might want a menu that changes depending on the application state. i.e: I can start or stop an animation by having the animation controls in the popup menu, and what the controls show would change every frame.
To achieve this, the library should simply return a "right click event" and let the programmer decide what to do with it. Ofc we should add an example on how to use that event.
I will toy with the code today and see if I can achieve something.
Are there new releases coaming to crates.io ?
Hey, been looking at the "persistence" feature. It seems to be missing the "egui/serde" feature? Seems to affect egui::Pos2 and egui::Vec2 uses
Hi,
first of all, this library is a great starting point for anyone wanting to do nodegraphs. So thanks for releasing it!
The Editor currently uses straight lines to connect the nodes. But Bezier Curves look so much nicer :)
Luckily egui is adding support for cubic bezier curves.
But since this is sadly only in their git, I am doing this writeup so once it ships, the implementation is straightforward.
Here I have linked a demo on how bezier curves can be set up to look like they do in other editors. You can move the endpoints to see how it feels. Notably, the control points are half the distance (s
in the demo) away from the endpoints, but on the same y
coordinate.
So, what has to be done here in the node graph?
In egui_node_graph/src/editor_ui.rs
; Instead of:
painter.line_segment([src_pos, dst_pos], connection_stroke);
Something like:
let s = 0.5 * src_pos.distance(dst_pos);
let control_pos1 = pos2(src_pos.x + s, src_pos.y);
let control_pos2 = pos2(src_pos.x - s, src_pos.y);
painter.add(Shape::CubicBezier(CubicBezierShape {
fill: Color32::TRANSPARENT,
points:[src_pos,control_pos1,control_pos2,dst_pos],
closed: false,
stroke: connection_stroke,
}));
egui_node_graph/egui_node_graph_example/src/app.rs
Lines 388 to 399 in 75308d0
If change it as follows, the switch doesn't work.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let graph_response = egui::CentralPanel::default()
.show(ctx, |ui| {
self.state
.draw_graph_editor(ui, AllMyNodeTemplates, &mut self.user_state)
})
.inner;
egui::TopBottomPanel::top("top").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::widgets::global_dark_light_mode_switch(ui);
});
});
I have two, possibly related, issues with UI ordering
Cables should render behind nodes. As you can see, if they are in front, it can look pretty janky.
If I click on the title "ObjRender", it brings the "Expression" node to the front.
I expect the UI to reflect what I can interact with. "Expression" should only be selected if I click where I can see it, on the left.
This is less of an issue more of a question:
I tried for a while to find a way to "bind" inputs to a widget in order to emulate the behaviour in Blender's node editor. In Blender it behaves as follows:
I suspected the way to do this is to remove the widget when the onConnectionsChange
callback fires, however I did not find a removeWidget
method that I could have used to remove that widget. Any pointers are welcome.
Currently draw_graph_editor
creates a CentralPanel to draw its stuff. In my case I would prefer if the the main application manages the container for for the graph_editor and just passes a Ui
to draw_graph_editor
. Is there a specific reason for the CentralPanel approach (like special requirement for the pan-zoom) ?
Hey! I wanted to use this library for my project, but it seems it's not very performant at high node counts. I noticed my computer struggling at 200 (unconnected) nodes, and it basically slowing to a standstill at 400.
If it's possible to add some performance features for large graphs, that would be appreciated. Not rendering nodes that aren't on-screen would be a logical place to start. (Though with the connections running across the screen that might be difficult.
NodeIds use slotmaps to hold references. Slotmaps return the same sequence of IDs for a given KeyType.
For example this runs with no panics
use slotmap::SlotMap;
fn main() {
let mut sm1 = SlotMap::new();
let mut sm2 = SlotMap::new();
let sm1_key1 = sm1.insert("foo");
let sm2_key1 = sm2.insert("bar");
assert_eq!(sm1_key1, sm2_key1);
}
Egui Node Graph statically assigns the same KeyType to the node_graph slotmap and therefore multiple node_graphs in the same process will return the same keys. Since these keys go into the same "global" egui widget key-space egui complains about identical keys.
If you right click a node output, it should show the add node ui, then automatically connect the added node to the clicked output if compatible.
I'm happy to pick up this work.
The example doesn't work with version 0.3.0. Are you gonna release a new version? or maybe add some more documentation to make it easier to get started?
so i made a node
MyNodeTemplate::JoinString => {
graph.add_input_param(
node_id,
"join".to_string(),
DataType::String,
ValueHolder::String {
value: " ".to_string(),
},
InputParamKind::ConnectionOrConstant,
true,
);
graph.add_input_param(
node_id,
"strings".to_string(),
DataType::List,
ValueHolder::List { value: vec![] },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "output".to_string(), DataType::String);
}
as you can see it should have 2 node inputs yet when i run the example
it only has 1 node input instead of 2
I tried swapping the order of the join and strings arguments yet it never shows the connection for the strings
0.19.0 was released a few days ago, and some packages (like bevy_egui
) have already moved to it, causing some incompatibilities if it's used with egui_node_graph
.
The upgrade is completely painless, and there do not appear to be any breakages.
I made a node that has one input that is InputParamKind::ConstantOnly. If I try to click near an output port of the same type as the input port I crash with the following:
at egui_node_graph-9cc9145316a46463\eeecd63\egui_node_graph\src\editor_ui.rs:189
187 โ .iter()
188 โ .find_map(|(port_id, _)| {
189 > let port_pos = port_locations[&port_id.into()];
190 โ if port_pos.distance(cursor_pos) < DISTANCE_TO_CONNECT {
191 โ Some(port_pos)
I can connect from an input back to the output without issues.So it appears that .find_map from line 188 does not exclude port_ids that do not have port_locations associated with them such as the InputParamKind::ConstantOnly, which should be filtered out prior to the mapping of existing locations.
Right now a Property can be either an Input or an Output, but not both. We should consider the option of allowing a property to be both an input and an output. This is used in the blueprint editor of UnrealEngine to declare the control flow of the script:
Blueprint example
To achieve this, we could unify InputParam
and OutputParam
structs, and instead differentiate between inputs and outputs in a new ParamKind
enum (which would have many possible values, like InputOnly
, OutputOnly
, InputOutput
, InputOrConstant(ValueType)
, etc).
iter_connections
current returns an iterator of (InputId, OutputId)
. I believe it would be more consistent to return (OutputId, InputId)
, and it matches the direction of connections in the graph.
On the other hand it's a bit of an unnecessary breaking change, but it's caught by the type system, and I think this library is young enough that it's worth it.
This was inspired by @philpax 's comment here. I did this here 32aa8d3, but it will conflict with #30, so waiting on that to make a PR.
Sorry about how inter-connected all my proposed changes are ๐
I want to render graphs using egui_node_graph - it would be helpful if egui_node_graph supported edge labeling/properties/directions out of the box
Iterating through all nodes in a graph does currently not ensure that all inputs of a node have been generated.
To simplify the generation of all values in a graph, an iterator that provides a topological ordering could be created.
Is there an easy way to create a graph state before running the application, either by loading from a file or just writing code. The example doesn't provide a clear way to just populate nodes from the outset (rather than drawing).
idk how to do this so i think it should be documented how to do it
I tried to figure it out my self, but some-have failed.
This is a great library ๐. Thank you!
I will report it because egui_node_graph_example
crashed.
thread 'main' panicked at 'NodeId index error for NodeId(1v1). Has the value been deleted?', /egui_node_graph/egui_node_graph/src/index_impls.rs:33:1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This is because MyResponse::SetActiveNode(node)
holds the NodeId
even if the node is deleted, and uses that NodeId
to access the deleted Node in the evaluate_node
function.
egui_node_graph/egui_node_graph_example/src/app.rs
Lines 378 to 390 in 8408503
if let Some(node) = self.state.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, 10.0),
egui::Align2::LEFT_TOP,
text,
egui::TextStyle::Button,
egui::Color32::WHITE,
);
+ }
}
Currently we can add parameters, but not remove them. It would be nice to be able to do both, to be able to do things like have math nodes that take a variable number of inputs, or to be able to grow/shrink the number of ouputs so that there's always another free one (for... reasons).
I've implemented this for output param's in 96dc5b6 - which depends on #30.
I'm working on my render graph editor. There are two special nodes called Input and Output. They should exist for all my node graphs, can not be created by the user, nor can be deleted.
I want to hide the close button for my Input/Output node. Currently I found no existing way to do this.
There can be a method fn can_delete(&self) -> bool
for NodeDataTrait
, so the nodes can decide whether it is deletable.
I implemented it on my fork. I can make a pr if you like.
Right now only the DeleteNode response is generated even if the node had connections. Would expect it to generate a disconnect event for every connection it had.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.