Hi,
Has anyone use SDL to develop a node graph UI with spline connecting the attributes between nodes ?
Is there a URL I can follow to read up more about such projects ?
Cheers
Hi,
Has anyone use SDL to develop a node graph UI with spline connecting the attributes between nodes ?
Is there a URL I can follow to read up more about such projects ?
Cheers
I have done this as a hobby project before and that was a fun project.
Object oriented programming in C++ was very helpful. Also I’d recommend you create a debug node that displays the data that it receives.
I’d rate it as a medium difficulty project: Not too intense if you are able to break it down into smaller problems to solve.
Things that might help:
Drag and Drop
Linked lists
Text Input
Graphical Design - The answer in this link shows a lot of example images that might help inspire your own design. (Communicating at a glance what the inputs and output do, what the node’s category is, etc). It looks like SDL_gfx has bezier curve support, but I never got that fancy.
I’ll try to come up with an example in 180 lines or less tomorrow afternoon… Do you mind if I write it in SDL3?
Wow, 180 lines was a bit too optimistic.
Anyhow, here’s a chicken-scratched Node UI.
I did not implement any data send/receive logic, but that’s just a couple of functions and a text IO class. The type of data you want to send will probably also change how you implement sharing.
The output should probably be a descendant of the Node class with a vector of link pointers to allow splitting output for multi-linkage.
Hope some of this helps. I at least had fun writing it out.
#include <SDL3/SDL.h>
#include <vector>
#include <cstdlib>
void fillColor(SDL_Renderer * renderer, SDL_Texture * texture, SDL_Color color)
{
SDL_SetRenderTarget(renderer, texture);
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 255);
SDL_RenderClear(renderer);
SDL_SetRenderTarget(renderer, NULL);
}
class Node
{
public:
Node(SDL_Renderer * screen)
{
renderer = screen;
pos = {0, 0, 15, 15};
image = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, pos.w, pos.h);
fillColor(renderer, image, {100, 200, 100, 255});
link = NULL;
backLink = NULL;
}
bool isHit(SDL_FPoint &pt)
{
return SDL_PointInRectFloat(&pt, &pos);
}
void clear()
{
SDL_DestroyTexture(image);
if(backLink->link == this)
{
backLink->unlink();
}
}
void setPos(float x, float y)
{
pos.x = x;
pos.y = y;
}
void unlink()
{
link = NULL;
backLink = NULL;
}
void linkTo(Node * other)
{
link = other;
other->backLink = this;
}
void draw()
{
SDL_RenderTexture(renderer, image, NULL, &pos);
if(link)
{
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderLine(renderer, pos.x + pos.w/2, pos.y + pos.h/2, link->pos.x + link->pos.w/2, link->pos.y + link->pos.h/2);
}
}
public:
SDL_Renderer * renderer;
SDL_Texture * image;
SDL_FRect pos;
Node * link;
Node * backLink;
};
std::vector <Node * > inputs;
class Attribute
{
public:
Attribute(SDL_Renderer * screen)
{
dragging = false;
value = 0;
renderer = screen;
pos = {0, 0, 80, 50};
image = NULL;
image = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, pos.w, pos.h);
fillColor(renderer, image, {200, 200, 120, 255});
input = new Node(renderer);
inputs.push_back(input);
output = new Node(renderer);
dragLink = NULL;
setPos(rand() % 900, rand() % 900);
}
void clear()
{
SDL_DestroyTexture(image);
image = NULL;
delete input;
delete output;
}
void setPos(float x, float y)
{
input->setPos(x - 5, y);
output->setPos(x + pos.w - 10, y);
pos.x = x;
pos.y = y;
}
void handleEvent(SDL_Event * ev)
{
switch(ev->type)
{
case SDL_EVENT_MOUSE_MOTION:
mouse = {ev->button.x, ev->button.y};
if(dragLink)
{
dragLink->setPos(mouse.x - 8, mouse.y - 8);
}
else if(dragging)
{
setPos(mouse.x, mouse.y);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if(output->isHit(mouse))
{
// Holding the output node.
if(output->link)
{
output->link->unlink();
}
output->unlink();
dragLink = new Node(renderer);
dragLink->setPos(mouse.x, mouse.y);
output->linkTo(dragLink);
}
else if(input->isHit(mouse))
{
if(input->backLink)
{
input->backLink->unlink();
input->backLink = NULL;
}
}
else if(SDL_PointInRectFloat(&mouse, &pos))
{
dragging = true;
}
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
if(dragLink)
{
if(output->link)
{
output->unlink();
}
size_t len = inputs.size();
for(size_t i = 0; i < len; i ++)
{
if(inputs[i]->isHit(mouse))
{
if(inputs[i]->backLink == NULL)
{
output->linkTo(inputs[i]);
}
}
}
dragLink->clear();
delete dragLink;
dragLink = NULL;
}
dragging = false;
break;
}
}
void draw()
{
if(image)
{
SDL_RenderTexture(renderer, image, NULL, &pos);
input->draw();
output->draw();
}
}
public:
SDL_Renderer * renderer;
SDL_Texture * image;
SDL_FRect pos;
SDL_FPoint mouse;
Node * input;
Node * output;
Node * dragLink;
bool dragging;
int value;
};
int main()
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window * win = SDL_CreateWindow("title", 1000, 1000, SDL_WINDOW_RESIZABLE);
SDL_Renderer * screen = SDL_CreateRenderer(win, 0, SDL_RENDERER_PRESENTVSYNC);
std::vector <Attribute *> Attributes;
Attributes.push_back(new Attribute(screen));
Attributes.push_back(new Attribute(screen));
Attributes.push_back(new Attribute(screen));
Attributes.push_back(new Attribute(screen));
bool run = true;
while(run)
{
SDL_Event ev;
while(SDL_PollEvent(&ev))
{
size_t len = Attributes.size();
for(size_t i = 0; i < len; i ++)
{
Attributes[i]->handleEvent(&ev);
}
switch(ev.type)
{
case SDL_EVENT_QUIT:
run = false;
break;
}
}
SDL_SetRenderDrawColor(screen, 95, 95, 125, 255);
SDL_RenderClear(screen);
size_t len = Attributes.size();
for(size_t i = 0; i < len; i ++)
{
Attributes[i]->draw();
}
SDL_RenderPresent(screen);
}
SDL_Quit();
}
I don’t have SDL3 but your example code might still useful for reference.
Thank you.
Here’s a video of the node’s behavior (I shot at 8 FPS thinking I had to turn it into a gif, I didn’t know that webm was supported here.)
These are some of the rules I applied, but you might want to change as the need arises:
The intent is that the yellow box would be the attribute or the function that you are trying to apply, so it would likely have a text box in between the two green nodes. The next step would be the Module class which would group several attributes together.
Part of why I skipped data sending is that through the backLink chain the Attributes/Modules would be able to request data, or you could send data forward instead. I don’t know the current use, so I don’t know if you would want the initial request for processing to come from a fully loaded push module at the start of the chain, or from a data starved module requesting data at the end of the chain. Or perhaps only updated when nodes are added or removed from the chain… Also I noticed I was already past my line-count goal and didn’t want to overwhelm the topic.
You can see in the video that I miss the node connector by several pixels a couple of times with the mouse, so it might be nice to implement a slightly larger hit box than what I went with. Or perhaps a larger invisible hitbox so the node points act more grabby.
This certainly is a primitive setup, but I hope it’s a good stepping stone.
Good example.
But unfortunately there is a bang when you first click on a left green rectangle.
Thanks, I’m pretty sure that you are referencing a bug that I quietly fixed/edited a couple of days ago.
I forgot to check if backLink was non-null before accessing an internal function there. Please grab a copy of the new version above, or add this to your code:
// in Mouse Button Down Event I added this if statement
// line 136
if(input->backLink)
{
input->backLink->unlink();
input->backLink = NULL;
}