Code:nodes.c
Writing content sucks. Knowledge is complex and interconnected, but language is linear. No matter how much time you spend editing, it will never flow as well as it could.
That's why I decided to make a new format for content:
- Everything is a directed graph.
- Nodes contain short pieces of text
- In the next versions, I plan to also have image nodes and calculator nodes.
- Think of it like a gigantic flowchart or mindmap, but you can fit more content than you ever could on a page.
I hope to turn this program into a web app, so that no one has to download & compile code just to view some content. For now, it's just a small experimental desktop app. It's very unfinished and I will probably change a lot in future versions. I haven't even written the usage instructions yet.
Author: User:Elie
Dependencies
Code
Data
Installed programs
This program only runs on Linux.
Code
/***
This is my new graph nodes program. I don't know what to call it yet.
To compile:
gcc nodes.c -o nodes -ffast-math -O -lm -lGL -lglut -lpthread
This program has runtime dependencies:
File in working directory: "font.data-uint8-1004x19"
Installed packages: 'zenity' and 'leafpad'
Copyright 2022, Elie Goldman Smith
This program is FREE SOFTWARE: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
***/
#include "fullscreen_main.h"
#include "text-quads.h"
#include <pthread.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define MAXTEXTLEVELS 7
const float TEXT_BOX_SIZES[MAXTEXTLEVELS] = {3, 4, 5, 7, 9, 11, 14}; // in 'em' units
typedef struct {
float x,y;
float dx,dy;
float size;
unsigned char r,g,b,a;
char *text;
int nTextLevels;
TQ_Drawable textRenders[MAXTEXTLEVELS];
} Node;
#define MAXNODES 4096
int nNodes=0;
Node nodes[MAXNODES];
typedef struct {
int from, to;
} Link;
#define MAXLINKS 4096
int nLinks=0;
Link links[MAXLINKS];
int focus = 0; // index of node that is in focus (selected)
void addNodeFrom(int id) {
if (nNodes >= MAXNODES) return;
if (nLinks >= MAXLINKS) return;
//focus=nNodes;
nodes[nNodes].x = _mouse_x;
nodes[nNodes].y = _mouse_y;
nodes[nNodes].size = nodes[id].size + RND()*0.02f;
int lum;
lum = nodes[id].r + (rand()&255)-128; if(lum<0)lum=0; if(lum>255)lum=255; nodes[nNodes].r = lum;
lum = nodes[id].g + (rand()&255)-128; if(lum<0)lum=0; if(lum>255)lum=255; nodes[nNodes].g = lum;
lum = nodes[id].b + (rand()&255)-128; if(lum<0)lum=0; if(lum>255)lum=255; nodes[nNodes].b = lum;
nodes[nNodes].a = 255;
links[nLinks].from = id;
links[nLinks].to = nNodes;
nLinks++;
nNodes++;
}
void toggleLink(int from, int to) {
if (to==from) return;
for (int i=0; i<nLinks; i++) {
if (links[i].to==to && links[i].from==from) { links[i] = links[--nLinks]; return; } // delete link XXX: gotta update toMoveLink in some rare cases
if (links[i].to==from && links[i].from==to) { links[i].to=to; links[i].from=from; return; } // flip direction
}
if (nLinks >= MAXLINKS) return;
links[nLinks].to = to;
links[nLinks].from = from;
nLinks++;
}
int nodeNearest(float x, float y) {
int which = 0;
float lowest = 1e36;
for (int i=0; i<nNodes; i++) {
float dsq = (nodes[i].x - x)*(nodes[i].x - x) + (nodes[i].y - y)*(nodes[i].y - y);
if (dsq < lowest) { lowest=dsq; which=i; }
}
return which;
}
void eraseNodeText(int id) {
free(nodes[id].text);
nodes[id].text = NULL;
for (int tl=0; tl < nodes[id].nTextLevels; tl++) tq_delete(&nodes[id].textRenders[tl]);
}
void deleteNode(int id) {
eraseNodeText(id);
nodes[id] = nodes[--nNodes];
memset(&nodes[nNodes], 0, sizeof(Node));
for (int i=0; i<nLinks; i++) {
if (links[i].to==id || links[i].from==id) links[i--] = links[--nLinks];
else {
if (links[i].to == nNodes) links[i].to = id;
if (links[i].from==nNodes) links[i].from=id;
}
} // XXX: also need to update references (toConnect, toPull, toMove, monitorEditNode, anything else?). If it's set to id, set to -1. If it's set to nNodes, set to id.
}
void genNodeTextRenders(int id) {
int tl=0;
while (tl<MAXTEXTLEVELS) {
nodes[id].textRenders[tl] = tq_centered_fitted(nodes[id].text, TEXT_BOX_SIZES[tl], TEXT_BOX_SIZES[tl]);
if (nodes[id].textRenders[tl++].isComplete) break;
}
nodes[id].nTextLevels = tl;
}
void saveToFile(const char *filename) {
FILE *f = fopen(filename,"w");
if (!f) return; // TODO: handle error case
fprintf(f, "view:\nf=%d\nnodes:\n", focus);
for (int i=0; i<nNodes; i++) {
fprintf(f, "i=%d c=%02X%02X%02X t=\"", i, (int)nodes[i].r, (int)nodes[i].g, (int)nodes[i].b);
char *p = nodes[i].text;
if (p) {
while (*p) {
if (*p=='\n') fputs("\\n", f);
else if(*p=='\"') fputs("\\\"",f);
else fputc(*p, f);
p++;
}
}fprintf(f,"\"\n");
}
fprintf(f,"connections:\n");
for (int i=0; i<nLinks; i++) fprintf(f, "a=%d b=%d\n", links[i].from, links[i].to);
fclose(f);
printf("Saved to file %s\n", filename);
}
void loadFile(const char *filename) { // TODO: respond more robustly (i.e. to avoid segfault when trying to load an invalid file)
int success = 0;
FILE *f = fopen(filename, "r");
if (f) {
if (fscanf(f,"view:\nf=%d\nnodes:\n",&focus)>0) {
// clear existing data
for (int i=0; i<nNodes; i++) {
eraseNodeText(i);
nodes[i].x = RND();
nodes[i].y = RND();
}
nNodes = nLinks = 0;
// read nodes from file
for (int i=0; i<MAXNODES; i++) {
int id; int r,g,b; char c;
if (fscanf(f, "i=%d c=%02X%02X%02X t=\"", &id, &r, &g, &b)>0) {
if (id >= 0 && id < MAXNODES) {
nodes[id].r=r; nodes[id].g=g; nodes[id].b=b; nodes[i].a=255;
if (nNodes <= id) nNodes = id+1;
size_t size;
FILE *ss = open_memstream(&nodes[id].text, &size);
while (1) {
c = fgetc(f);
if (c == '\"' || c == EOF) break;
if (c == '\\') {
c = fgetc(f);
if (c == EOF) break;
if (c == '\"') fputc('\"',ss);
else if (c == 'n' ) fputc('\n',ss);
else if (c == '\\') fputc('\\',ss);
else {
fputc('\\',ss);
fputc(c, ss);
}
} else fputc(c, ss);
} fclose(ss);
}
} else i=MAXNODES; // reached the line "connections:" so end this for-loop
while ((c=fgetc(f)) != '\n' && c != EOF); // skip to the next line
}
// read the list of connections from file
for (int i=0; i<MAXLINKS; i++, nLinks++) {
int a,b;
if (fscanf(f,"a=%d b=%d\n",&a,&b)==2) {
links[i].from = a;
links[i].to = b;
} else break;
} success=1;
}
fclose(f);
if (success) {
for (int i=0; i<nNodes; i++) genNodeTextRenders(i); // this is done at the end because it might need a lot of memory (fclose() would have freed some)
printf("Opened file %s\n", filename);
} else printf("Invalid file %s\n", filename);
} else perror(filename);
}
void drawCircle(float x, float y, float radius) {
glBegin(GL_LINE_LOOP);
float da=(float)M_PI/16.f;
for (float a = da*0.5f; a < (float)M_PI*2.f; a += da) glVertex2f(x + cosf(a)*radius, y + sinf(a)*radius);
glEnd();
}
const char * monitorFileName = "/tmp/edit-text-node";
volatile struct timespec monitorFileTime = {0};
volatile int monitorEditNode = -1;
void editTextNode(int id) {
const char *editor_names[] = {"leafpad","defaulttexteditor","gedit","geany","notepad++","wordpad","notepad","nano","vim","vi","emacs",NULL};
FILE *f = fopen(monitorFileName, "w");
if (f) {
if (nodes[id].text) fputs(nodes[id].text, f);
fclose(f);
struct stat st;
stat(monitorFileName, &st);
monitorFileTime = st.st_mtim;
monitorEditNode = id;
if (fork()==0) {
for (int i=0; editor_names[i]; i++) execlp(editor_names[0], editor_names[0], monitorFileName, (char*)NULL);
printf("Can't edit node: No text editor found.\n");
}
}
else {} // TODO: handle error case
}
void *fileMonitor(void *ptr) { // pthread
while (1) {
if (monitorEditNode >= 0) {
struct stat st;
stat(monitorFileName, &st);
if (st.st_mtim.tv_sec != monitorFileTime.tv_sec || st.st_mtim.tv_nsec != monitorFileTime.tv_nsec) {
char *str = malloc(st.st_size+1);
if (!str) return NULL; // TODO: handle error case
FILE *f = fopen(monitorFileName, "r");
if(!f){free(str);return NULL;}// TODO: handle error case
int n = fread(str, 1, st.st_size, f);
str[n]=0;
fclose(f);
eraseNodeText(monitorEditNode);
nodes[monitorEditNode].text = str;
genNodeTextRenders(monitorEditNode);
monitorFileTime = st.st_mtim;
monitorEditNode = -1;
}
} sleep(1);
}
}
void init() {
tq_init(); glDisable(GL_TEXTURE_2D);
show_mouse();
memset(nodes, 0, sizeof(nodes)); // this also initializes any pointers to NULL, so it's safe to call free() on them at any time
memset(links, -1,sizeof(links)); // -1 is safe, will be interpereted as 'not a link'
if (_global_argc==2) loadFile(_global_argv[1]);
if (nNodes < 1) {
// root node
nodes[nNodes].size = 0.04f;
nodes[nNodes].r = 255;
nodes[nNodes].g = 255;
nodes[nNodes].b = 255;
nodes[nNodes].a = 255;
nNodes++;
}
pthread_t fm; // file monitor thread (for editing a node)
pthread_create(&fm,NULL,fileMonitor,NULL);
#ifdef USE_MULTISAMPLING
glLineWidth(1.5f);
#endif
}
void draw() {
//==User interface==
// select node (Left click)
if (_mouse_button_map[0]==KEY_FRESHLY_PRESSED) focus = nodeNearest(_mouse_x, _mouse_y);
// drag node (Right click)
static int toPull = -1;
if (_mouse_button_map[2]==KEY_FRESHLY_PRESSED) toPull = nodeNearest(_mouse_x, _mouse_y);
if (_mouse_button_map[2] && toPull >= 0) { nodes[toPull].x = _mouse_x; nodes[toPull].y = _mouse_y; } else toPull = -1;
// new node (N)
if (keymap['N']==KEY_FRESHLY_PRESSED) addNodeFrom(focus);
// edit node text (E)
if (keymap['E']==KEY_FRESHLY_PRESSED) editTextNode(focus);
// connect nodes (C, and then Enter or Space)
static int toConnect = -1;
if (keymap['C']==KEY_FRESHLY_PRESSED) {
if (toConnect < 0) toConnect = focus;
else toConnect = -1;
}
if (toConnect >= 0) {
if (keymap[13]==KEY_FRESHLY_PRESSED) { toggleLink(toConnect, focus); toConnect = -1; }
if (keymap[' ']==KEY_FRESHLY_PRESSED){ toggleLink(focus, toConnect); toConnect = -1; }
}
// move node (M, and then Enter or Space)
static int toMove = -1;
static int toMoveLink = -1;
if (keymap['M']==KEY_FRESHLY_PRESSED) {
if (toMove != focus) {
toMove = focus;
toMoveLink = -1;
}
while (++toMoveLink < nLinks) { if (links[toMoveLink].to == toMove) break; }
if (toMoveLink >= nLinks) toMoveLink = toMove = -1;
}
if (toMove >= 0 && toMove != focus) {
if (keymap[13]==KEY_FRESHLY_PRESSED) { links[toMoveLink].from=focus; toMoveLink=toMove=-1; }
if (keymap[' ']==KEY_FRESHLY_PRESSED){ links[toMoveLink].from=toMove; links[toMoveLink].to=focus; toMoveLink=toMove=-1; }
}
// delete node (Delete)
if (keymap[127]==KEY_FRESHLY_PRESSED && nNodes>1) { deleteNode(focus); focus = nodeNearest(0,0); }
// help (F1)
if (special_keymap[GLUT_KEY_F1]==KEY_FRESHLY_PRESSED) {} // TODO: implement. First have to write the help info.
// back button (Backspace)
if (keymap[8] == KEY_FRESHLY_PRESSED) {} // TODO: implement. Need stack and need to figure out what to do in case of deleted nodes
// save file (Ctrl-S or F5)
if (keymap[19] ==KEY_FRESHLY_PRESSED || (special_keymap[114] && keymap['S']==KEY_FRESHLY_PRESSED) || special_keymap[GLUT_KEY_F5]==KEY_FRESHLY_PRESSED) {
char filename[4096];
FILE *f = popen("zenity --file --title 'Save as...' --save --confirm-overwrite 'Overwrite existing file?' --maximized --close-on-unfocus", "r");
if (f) {
char *c = fgets(filename,4096,f);
if (pclose(f)==0) {
int len=strlen(filename);
if (len>1) {
filename[len-1] = 0; // to remove the newline
saveToFile(filename);
} else puts("Empty filename");
} else puts("No filename selected");
}
}
// load file (Ctrl-O or F6)
if (keymap[15] ==KEY_FRESHLY_PRESSED || (special_keymap[114] && keymap['O']==KEY_FRESHLY_PRESSED) || special_keymap[GLUT_KEY_F6]==KEY_FRESHLY_PRESSED) {
char filename[4096];
FILE *f = popen("zenity --file --title 'Open graph file...' --maximized --close-on-unfocus", "r"); // or could also use --on-top
if (f) {
char *c = fgets(filename,4096,f);
if (pclose(f)==0) {
int len=strlen(filename);
if (len>1) {
filename[len-1] = 0; // to remove the newline
loadFile(filename);
} else puts("Empty filename");
} else puts("No filename selected");
} // XXX: need to decide: which messages should be in stdout vs stderr
}
//==Physics==
// center the graph
if (!_mouse_button_map[2]) {
static float dx=0; dx *= 0.875f; dx += nodes[focus].x / -128;
static float dy=0; dy *= 0.875f; dy += nodes[focus].y / -128;
for (int i=0; i<nNodes; i++) {
nodes[i].x += dx;
nodes[i].y += dy;
}
}
// establish which nodes are "relevant" aka potentially onscreen
static Node* r[MAXNODES];
int nRelevant=0;
for (int i=0; i<nNodes; i++) {
if (nodes[i].x*nodes[i].x + nodes[i].y*nodes[i].y < 4.f) {
r[nRelevant++] = &nodes[i];
nodes[i].size=0.5f;
}
}
// apply bond forces
for (int i=0; i<nLinks; i++) {
float dx = nodes[links[i].to].x - nodes[links[i].from].x;
float dy = nodes[links[i].to].y - nodes[links[i].from].y;
float inv = 0.002f / sqrtf(dx*dx+dy*dy+1.f);
dx *= inv; dy *= inv; // dy += 0.0002f; // bias to make the graph flow in one direction slightly
nodes[links[i].to ].dx -= dx;
nodes[links[i].to ].dy -= dy;
nodes[links[i].from].dx += dx;
nodes[links[i].from].dy += dy;
}
// apply repel forces
for (int h = 0; h<nRelevant; h++) {
for (int i=h+1; i<nRelevant; i++) {
float dx = r[i]->x - r[h]->x;
float dy = r[i]->y - r[h]->y;
float maxsize = fabs(dx) > fabs(dy) ? fabs(dx) : fabs(dy); maxsize *= 0.44f;
if (r[h]->size > maxsize) r[h]->size = maxsize;
if (r[i]->size > maxsize) r[i]->size = maxsize;
if (maxsize < 0.0001f) { r[i]->x += RND()*0.0001f; r[i]->y += RND()*0.0001f; }
float dsq = dx*dx+dy*dy;
float inv = 1.f/sqrtf(dsq+0.01f); // for normalizing
inv *= 0.00001f*inv*inv; // for inverse square law
float falloff = 0.142857f*(7.f - r[i]->x*r[i]->x - r[i]->y*r[i]->y - r[h]->x*r[h]->x - r[h]->y*r[h]->y);
if (falloff < 0.f) falloff = 0.f;
inv *= falloff*falloff; // for clustering in distance
dx *= inv; dy *= inv;
r[h]->dx -= dx;
r[h]->dy -= dy;
r[i]->dx += dx;
r[i]->dy += dy;
}
}
// update positions
for (int i=0; i<nNodes; i++) {
nodes[i].x += nodes[i].dx;
nodes[i].y += nodes[i].dy;
nodes[i].dx *= 0.9375f;
nodes[i].dy *= 0.9375f;
}
//==Rendering==
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
// this projection matrix gives us "aspect-ratio-independent" normalized coordinates instead of the standard "normalized device coordinates"
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glScalef((GLfloat)_screen_size/(GLfloat)_screen_x, (GLfloat)_screen_size/(GLfloat)_screen_y, 1.f);
// draw the links
glBegin(GL_LINES);
glColor3f(1.f,1.f,1.f);
for (int i=0; i<nLinks; i++) {
if (links[i].to >= 0 && links[i].from >= 0) {
// main line
glVertex2f(nodes[links[i].to ].x, nodes[links[i].to ].y);
glVertex2f(nodes[links[i].from].x, nodes[links[i].from].y);
// add subtle chevron at the midpoint of the line, to indicate direction
float mx =(nodes[links[i].to].x + nodes[links[i].from].x)*0.5f;
float my =(nodes[links[i].to].y + nodes[links[i].from].y)*0.5f;
float dx = nodes[links[i].to].x - nodes[links[i].from].x;
float dy = nodes[links[i].to].y - nodes[links[i].from].y;
float norm = 0.007f / sqrtf(dx*dx + dy*dy);
dx *= norm;
dy *= norm;
glVertex2f(mx-dy-dx, my+dx-dy);
glVertex2f(mx +dx, my +dy);
glVertex2f(mx +dx, my +dy);
glVertex2f(mx+dy-dx, my-dx-dy);
}
}
glEnd();
// show new connection to be added (if there is one pending)
if (toConnect >= 0) {
glColor3f(0.7f,0.f,0.f);
glBegin(GL_LINES);
glVertex2f(nodes[toConnect].x, nodes[toConnect].y);
glVertex2f(nodes[focus].x, nodes[focus].y);
glEnd();
}
// show connection that may be moved (if there is one pending)
if (toMoveLink >= 0) {
glColor3f(0.0f,0.5f,0.5f);
glBegin(GL_LINES);
glVertex2f(nodes[links[toMoveLink].from].x, nodes[links[toMoveLink].from].y);
glVertex2f(nodes[links[toMoveLink].to ].x, nodes[links[toMoveLink].to ].y);
glEnd();
}
// precalculate screen boundaries
float bx = _screen_x / _screen_size;
float by = _screen_y / _screen_size;
// draw the nodes
for (int i=0; i<nRelevant; i++) { // this implementation uses Immediate Mode. XXX: instead of this, maybe use a vertex array with GL_POINTS, use point sprites with a shader that makes the rounded square shape?
if (r[i]->size > 0) {
float x1 = r[i]->x-r[i]->size;
float y1 = r[i]->y-r[i]->size;
float x2 = r[i]->x+r[i]->size;
float y2 = r[i]->y+r[i]->size;
float c = r[i]->size*0.57f; if (c>0.01f) c=0.01f; // corner size
if (x1<bx && x2>-bx && y1<by && y2>-by) {
glBegin(GL_POLYGON);
glColor3ub(r[i]->r, r[i]->g, r[i]->b);
glVertex2f(x2-c, y2 );
glVertex2f(x1+c, y2 );
glVertex2f(x1 , y2-c);
glVertex2f(x1 , y1+c);
glVertex2f(x1+c, y1 );
glVertex2f(x2-c, y1 );
glVertex2f(x2 , y1+c);
glVertex2f(x2 , y2-c);
glEnd();
}
}
}
// draw the text on the nodes
glPushAttrib(GL_ENABLE_BIT); tq_mode();
glColor3ub(255,255,255);
for (int i=0; i<nRelevant; i++) {
// filter out nodes with nothing to show
if (r[i]->textRenders[0].n <= 0) continue;
if (r[i]->x - r[i]->size > bx) continue;
if (r[i]->x + r[i]->size < -bx) continue;
if (r[i]->y - r[i]->size > by) continue;
if (r[i]->y + r[i]->size < -by) continue;
// decide which textRender to use, if any.
const float FONT_SIZE = 0.015f; // (nominal minimum)
int tl = r[i]->nTextLevels-1;
while(tl >= 0 && TEXT_BOX_SIZES[tl]*FONT_SIZE*0.5f > r[i]->size) tl--;
if (tl >= 0) {
// decide whether to make the text black or white
if (0.2126f*r[i]->r + 0.7152f*r[i]->g + 0.0722f*r[i]->b > 144) glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); else glBlendEquation(GL_FUNC_ADD);
// render
glPushMatrix();
glTranslatef(r[i]->x, r[i]->y, 0.f);
float scale = r[i]->size * 2.f / TEXT_BOX_SIZES[tl];
glScalef(scale, scale, 1.f);
tq_draw(r[i]->textRenders[tl]);
tq_draw(r[i]->textRenders[tl]); // TODO: instead of drawing twice, make a higher-contrast shader in text-quads.h
glPopMatrix();
}
}
glPopAttrib();
// highlight focused node
glColor3f(1.f,1.f,0.f);
drawCircle(nodes[focus].x, nodes[focus].y, nodes[focus].size*(float)M_SQRT2);
// highlight node to be connected
glColor3f(1.f,0.f,0.f);
drawCircle(nodes[toConnect].x, nodes[toConnect].y, nodes[toConnect].size*(float)M_SQRT2);
// highlight node being edited
glColor3f(0.f,0.f,1.f);
drawCircle(nodes[monitorEditNode].x, nodes[monitorEditNode].y, nodes[monitorEditNode].size*(float)M_SQRT2);
// highlight node to be moved
glColor3f(0.0f,0.5f,0.5f);
drawCircle(nodes[toMove].x, nodes[toMove].y, nodes[toMove].size*(float)M_SQRT2);
}
void done() {
for (int i=0; i<nNodes; i++) eraseNodeText(i);
tq_done();
}