Code:nodes.c

From the change wiki
Revision as of 09:34, 30 November 2022 by Elie (talk | contribs) (Created page with "<syntaxhighlight lang="c"> /*** 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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
/***
 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();
}