Node graph UI example ?

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?

1 Like

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();
}
2 Likes

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:

  • An input node (left) that is already linked will refuse further connections from output nodes(right).
  • Clicking any node point will remove existing connections.
  • Only output nodes can spawn a new connection.

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.

1 Like

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;
	}
1 Like

@ GuildedDoughnut

Thanks