fattorino / imnodeflow Goto Github PK
View Code? Open in Web Editor NEWNode based editor/blueprints for ImGui
License: MIT License
Node based editor/blueprints for ImGui
License: MIT License
Hello,
please add in your documentation than your lib is requiring c++20 at least.
and maybe other minimal requirements
Natively support pins of type bool
It would be useful to have the option to forward constructor arguments to the derived class when performing addNode
to get more reusable behaviour out of user defined node types without having to pass them in through some alternate mechanism.
Thanks for this amazing library.
ImNodeFlow is very easy for me to use.
I have some ideas regarding the enum "ConnectionFilter".
I need various types of nodes for complex pipeline design.
My nodes might have nested relationships (parent/children).
I hope the ConnectionFilter can be a std::function that determines whether the nodes are connectable or not (based on port type or parent type), rather than just using an enum.
Not sure if anyone else has any other ideas?
Hi and first of all: thanks for the awesome project! I am currently looking for a node editor for a small project of mine. In this project I would need to connect one output pin of one node (let's call it node 1) to the input pin of another node (node 2) and than feed the outcome of the second node back into the first node. Would it be possible to move the pins around inside a node like in the attached sketch ("I": input pin, "O": output pin) to not clutter the diagram with crossing links?
I think this part of the code is why the second out pin is not calling m_val = m_behaviour()
template<class T>
const T &OutPin<T>::val()
{
if (std::find((*m_inf)->get_recursion_blacklist().begin(), (*m_inf)->get_recursion_blacklist().end(), m_parent->getUID()) == (*m_inf)->get_recursion_blacklist().end())
{
(*m_inf)->get_recursion_blacklist().emplace_back(m_parent->getUID());
m_val = m_behaviour();
}
return m_val;
}
From preliminary testing despite the documentation on addIN
and addOUT
I seem to be able to dynamically add pins at runtime outside of the constructor. It seems like this just works and maybe there's some warrant to verifying this with a more extensive test as I just tested adding a single in/out to a node that was already "wired" up to other nodes.
I understand that probably the more conceptually correct way to do this would be by replacing the node with a new one with the right pin configurations. But there is no way to remove nodes from the view. And if that was added another problem would be that the connections on the existing node have to be re-routed to the new node. Which is why I think fully supporting adding and removing of IN/OUT pins outside of the constructor would be a preferred approach.
Would be interested to hear your thoughts about this.
The documentation is not up to date in the master branch
Getting a Pin that doesn't exist simply crashes the program
Version: master branch
ImGui: 1.90.5-docking
Connecting a OutPin with 2 or more InPin's and then close the program. The deconstructor from ImFlow::Link produces a "Access violation on reading location ..."
.
One fix that I tried, was to replace m_left
with m_right
but that seems to break other things, like the node deletion logic.
Link::~Link() {
- m_left->deleteLink();
+ m_right->deleteLink();
}
I minimized my code to be still functional and replicate the issue here. It is not the most good looking code, but should be readable enough.
Also, I added all c++ code in the spoilers following.
#include "Window.h"
int main() {
CWindow window{};
window.run();
return 0;
}
#ifndef BOOLEANTABLE_WINDOW_H
#define BOOLEANTABLE_WINDOW_H
#include <stdexcept>
#include <SDL3/SDL.h>
#include <glad/gl.h>
#include <imgui.h>
#include <imgui_impl_sdl3.h>
#include <imgui_impl_opengl3.h>
#include <ImNodeFlow.h>
const int cWinHeight = 400;
const int cWinWidth = 600;
class CWindow {
SDL_Window* window;
SDL_GLContext context;
bool running = true;
std::shared_ptr<ImFlow::ImNodeFlow> imNodeFlow;
std::vector<std::string> variables;
public:
explicit CWindow();
~CWindow();
void run();
};
#endif //BOOLEANTABLE_WINDOW_H
#include "Window.h"
#include "nodes/AndNode.h"
CWindow::CWindow() : imNodeFlow(std::make_shared<ImFlow::ImNodeFlow>("Main Grid")) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMEPAD) != 0) {
throw std::runtime_error("Could not initialize SDL3");
}
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
window = SDL_CreateWindow("Boolean Tables", cWinWidth, cWinHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (window == nullptr) {
throw std::runtime_error("Could not create a SDL3 Window");
}
context = SDL_GL_CreateContext(window);
if (context == nullptr) {
throw std::runtime_error("Could not create a OpenGL Context");
}
SDL_GL_MakeCurrent(window, context);
SDL_GL_SetSwapInterval(1);
int gl_version = gladLoadGL(SDL_GL_GetProcAddress);
if (gl_version == 0) {
throw std::runtime_error("Could not initialize OpenGL context");
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
ImGui::StyleColorsDark();
// When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
ImGuiStyle& style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// Setup Platform/Renderer backends
ImGui_ImplSDL3_InitForOpenGL(window,context);
ImGui_ImplOpenGL3_Init();
imNodeFlow->addNode<CAndNode>(ImVec2{2, 2});
imNodeFlow->droppedLinkPopUpContent([this](ImFlow::Pin *pin) {
if (ImGui::Button("And")) {
auto node = this->imNodeFlow->placeNode<CAndNode>();
pin->createLink(node->first.get());
}
});
}
CWindow::~CWindow() {
imNodeFlow.reset();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
}
void CWindow::run() {
ImGuiIO& io = ImGui::GetIO(); (void)io;
while(running) {
for(SDL_Event event; SDL_PollEvent(&event);) {
ImGui_ImplSDL3_ProcessEvent(&event);
switch (event.type) {
case SDL_EVENT_QUIT:
running = false;
return;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (event.window.windowID == SDL_GetWindowID(window)) {
running = false;
return;
}
break;
default:
break;
}
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Boolean Editor");
imNodeFlow->update();
ImGui::End();
ImGui::Render();
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
}
SDL_GL_SwapWindow(window);
}
}
#ifndef BOOLEANTABLE_ANDNODE_H
#define BOOLEANTABLE_ANDNODE_H
#include <ImNodeFlow.h>
class CAndNode : public ImFlow::BaseNode {
public:
std::shared_ptr<ImFlow::InPin<bool>> first;
std::shared_ptr<ImFlow::InPin<bool>> second;
CAndNode();
};
#endif //BOOLEANTABLE_ANDNODE_H
#ifndef BOOLEANTABLE_ANDNODE_H
#define BOOLEANTABLE_ANDNODE_H
#include <ImNodeFlow.h>
class CAndNode : public ImFlow::BaseNode {
public:
std::shared_ptr<ImFlow::InPin<bool>> first;
std::shared_ptr<ImFlow::InPin<bool>> second;
CAndNode();
};
#endif //BOOLEANTABLE_ANDNODE_H
I am not sure if it is the expected behavior for everyone or just in my usage scenarios.
By the design of my game engine, the user first selects some nodes in the graph, then the inspector window shows all properties of the selected nodes.
But when the user wants to click a button on the inspector window, suddenly the nodes are not selected anymore, and the inspector window does not show any properties of the nodes anymore (because the selection is canceled).
I added the IsWindowHovered() condition when selecting the node in "void BaseNode::update()", which fixes the problem:
// Orig:
if (!ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ...)
selected(false);
// My check:
if (ImGui::IsWindowHovered() && !ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ...)
selected(false);
And for the destroy(), I added the IsWindowFocused() condition
// Orig:
if (ImGui::IsKeyPressed(ImGuiKey_Delete) && !ImGui::IsAnyItemActive() && isSelected())
destroy();
// My check:
if (IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete) && !ImGui::IsAnyItemActive() && isSelected())
destroy();
But I am not sure if it is a bug or the expected behavior, so I am just reporting it here.
Was trying out the following examples and they seem to crash when connecting pins to other objects with a Pin UID not found error.
ImNodeFlow.inl:189: const T& ImFlow::BaseNode::getInVal(const U&) [with T = double; U = std::__cxx11::basic_string<char>]: Assertion `it != m_ins.end() && "Pin UID not found!"' failed.
Using the following Node definition
struct Test : ImFlow::BaseNode
{
void draw()
{
ImGui::Text("test");
showIN<double>("INPUT", 0.0, ImFlow::ConnectionFilter::SameType());
showOUT<double>("OUTPUT", [this](){ return getInVal<double>("INPUT"); });
}
};
I noticed this while also trying out the notion of wrapping lambdas up as nodes, which I would also imagine should work. And it does render but has the same crash.
INF.addLambda([](ImFlow::BaseNode* self){
ImGui::Text("lambda");
self->showIN<double>("INPUT", 0.0, ImFlow::ConnectionFilter::SameType());
self->showOUT<double>("OUTPUT", [self](){ return self->getInVal<double>("INPUT"); });
}, {0,0});
// with this added to ImNodeFlow class
template <typename L, typename B = BaseNode>
struct NodeWrapper : B, L
{
NodeWrapper(L&& l): BaseNode(), L(std::forward<L>(l)) {}
void draw() { L::operator()(this); }
};
template<typename L>
std::shared_ptr<NodeWrapper<L>> addLambda(L&& lambda, const ImVec2& pos)
{
return addNode<NodeWrapper<L>>(pos, std::forward<L>(lambda));
}
Is this MIT or what license do you want to use?
Allow an Input Pin from a node to be able to be fed from an Output Pin of the same node. This would allow modeling of things like PID which take output signals as feedback for control modulation.
When calling ImFlow::ImNodeFlow::update()
inside an ImGui window, that window no longer updates its position and size within imgui.ini
static ImFlow::ImNodeFlow myGrid;
if (ImGui::Begin("Test Window"))
{
myGrid.update();
}
ImGui::End();
Reposition/resize the "Test Window" and restart the application. The windows' new position/size will not be retained between executions.
If the call to update()
is removed, the retention of the windows position/size behaves normally again.
ImGui Info | |
---|---|
Version | 1.90 WIP |
Num | 18993 |
I am new to ImGUI, but it has been pretty easy to figure out. What I am not figuring out is changing the size of the window that the grid is rendered in. Or in setting its position. Before calling the ImNodeFlow::update(), I have the following to maximize the window:
const ImGuiViewport* MainViewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(MainViewport->Size, ImGuiCond_FirstUseEver);
But the ImGUI window for ImNodeFlow never matches the size of main viewport. What am I doing wrong? Thank you.
Ability to add/remove nodes from the Node editor via a removeNode
interface.
I was playing around with how easy it would be. I've got it just searching, finding, and erasing the node from the ImNodeFlow context. Works fine if there is no connections but runs into some UB when the next update loop comes through and uses the deleted Pin in the Link. Haven't gotten further than a quick debugger to take a quick peek at what is going wrong. I'll put up a branch and continue off it when my cold goes away.
tested your simple example but i cannot move nodes around or make connections I can just zoom in and any other input is ignored.
the fact that zooming works makes me guess my imgui implementation is ok as it also works correctly if I make normal imgui widgets.
I probably I forgot to initialize something... does it ring some bells?
this is the nodeflow related code just as in the example
#pragma once
#include <ImNodeFlow.h>
namespace Node_Editor {
using namespace ImFlow;
ImNodeFlow* nodeFlow;
class SimpleSum : public BaseNode
{
public:
SimpleSum()
{
setTitle("Simple sum");
setStyle(NodeStyle::green());
addIN<int>("IN_VAL", 0, ConnectionFilter_Int);
addOUT<int>("OUT_VAL", ConnectionFilter_Int)
->behaviour([this]() { return getInVal<int>("IN_VAL") + m_valB; });
}
void draw() override
{
ImGui::SetNextItemWidth(100.f);
ImGui::InputInt("##ValB", &m_valB);
}
private:
int m_valB = 0;
};
void init() {
nodeFlow = new ImNodeFlow("node Editor");
nodeFlow->addNode<SimpleSum>(ImVec2(0,0));
nodeFlow->addNode<SimpleSum>(ImVec2(100, 100));
}
void shutdown() {
delete nodeFlow;
}
void Exec() {
nodeFlow->update();
}
}
Hello,
thanks for this good lib.
btw when you delete a node by using the key "delete", cause a crash.
its because you destroy a node from him internally while iterated from a loop.
m_inf->getNodes().erase(m_uid)
Destroying a node internally via a global pointer is a dangerous behavior, and here cause the issue.
after that line, m_inf point on a release memory and cause the crash for next calls after line 186 of ImFlowNode.cpp
for me,
you need to mark a node as "to delete"
and delete it from the editor after nodes iteration
i have fixed it here (but maybe not the way you like :) )
ps : a demo app branch can be cool for see how to use it and test it
First, thank you for this awesome project!
The problem is that I want to deal with many vertices. This is why I get the following assertion error.
As the comment suggests, it is a good idea to skip drawing nodes that are outside of the editor view.
I am not quite familiar with DearImgui, in particular with this project -- I just started using it. But I think that it is possible to skip calls here: before update
, it can check whether a node inside the render window and draw it only if it is in inside.
If this edit does not breaks imgui/ImNodeFlow architecture/invariants, I even can try to implement it myself and create a PR.
Thank you!
Can someone please give a minimal working sample on how to use this library. Readme has a code on how to create a custom node, what about something that just works? I've searched codebase here with no success.
I noticed that my nodes are being executed in completely random order even tho they are created in the exact same way every time:
std::shared_ptr<PathTracerOutputNode> inputNode = m_Handler->addNode<PathTracerOutputNode>({ 0, 400 }, &m_NodeQueue, m_InputImage);
std::shared_ptr<BloomNode> bloomNode = m_Handler->addNode<BloomNode>({ 170, 400 }, &m_NodeQueue, VkExtent2D{ 900, 900 });
std::shared_ptr<TonemapNode> tonemapNode = m_Handler->addNode<TonemapNode>({ 370, 400 }, &m_NodeQueue, VkExtent2D{ 900, 900 });
std::shared_ptr<OutputNode> outputNode = m_Handler->addNode<OutputNode>({ 500, 400 }, &m_NodeQueue, &m_OutputImage);
bloomNode->inPin("Input")->createLink(inputNode->outPin("Output"));
tonemapNode->inPin("Input")->createLink(bloomNode->outPin("Output"));
outputNode->inPin("Window Output")->createLink(tonemapNode->outPin("Output"));
Each node type has draw function and I need them to execute in the correct order, so from left to right, I first need output from path tracer to then be able to tonemap it, but sometimes for whatever reason tonemapping is executed first.
What I'm doing rn inside those draw functions is:
void draw() override
{
// ImGui Stuff
...
auto func = [this]() {
// Rendering Stuff, Get handle and run compute shaders
VkImage handle = getInVal<Image*>("Input")->GetImage();
...
};
m_Queue->PushTask(func); // Push rendering stuff into queue that is executed later on
}
I have to push rendering stuff into queue and not just execute it right away because vulkan doesn't allow that.
In documentation I read that What is outputted is defined by the pin behaviour, So I thought that maybe pushing func into a queue inside behaviour() instead of draw() would solve the problem but it didn't. Nodes are still executed in random order:
addOUT<Image*>("Output")->behaviour(
[this]()
{
// Makes sure to evaluate prev node?
getInVal<Image*>("Input");
auto func = [this]() {
// Rendering Stuff, Get handle and run compute shaders
VkImage handle = getInVal<Image*>("Input")->GetImage();
...
};
m_Queue->PushTask(func); // Push rendering stuff into queue that is executed later on
return &m_OutputImage;
}
);
If I just print whenever I call PushTask() in these nodes I get different output each time I run the app:
( Correct output is Bloom -> Tonemap -> Output)
1st run:
[15:48:57] Pushed Viewport Output
[15:48:57] Pushed Bloom
[15:48:57] Pushed Tonemapping
2nd run:
[15:49:38] Pushed Bloom
[15:49:38] Pushed Tonemapping
[15:49:38] Pushed Viewport Output
3rd run:
[15:50:11] Pushed Bloom
[15:50:11] Pushed Viewport Output
[15:50:11] Pushed Tonemapping
So, am I doing something wrong, or nodes are just supposed to run in random order (which wouldn't make any sense)? What am I supposed to do to first evaluate all input nodes? I just need things Pushed into m_Queue in correct order, I would really appreciate any help.
Some things seems to be missing from the actual documentation like the possibility to add links programmatically to nodes.
I need to serialize and deserialize nodegraphs so I need to recreate connections programmatically.
It would be great to have a more complex example that show off the entire api some kind of imgui demo but for imNodeFlow.
On line 490 in the header file this should be changed to
std::vector<PinUID> m_nodeRecursionBlacklist;
Also if there are nodes with the same out pin name this will not work.
Maybe do something like using the parent uid and the out uid:
template<class T>
const T &OutPin<T>::val()
{
PinUID po_uid = m_parent->getUID() + m_uid; // po -> parent + out pin uid
if (std::find((*m_inf)->get_recursion_blacklist().begin(), (*m_inf)->get_recursion_blacklist().end(), po_uid) == (*m_inf)->get_recursion_blacklist().end())
{
(*m_inf)->get_recursion_blacklist().emplace_back(po_uid);
m_val = m_behaviour();
}
return m_val;
}
I was pleasantly surprised that node editors within nodes worked pretty well.
There are a few quirks though listed below, and possibly others. I will refer to the node editor within a node as the "inner node editor" and the editor that contains that node the "outer node editor"
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.