$ git clone http://gcodetool.ion.nu/gcodetool.git
commit d9df22e382a0d476f71bc4fbd6903b60fc2f5d85
Author: Alicia <...>
Date:   Sun Oct 6 21:41:50 2019 +0000

    Added a preview subcommand. Not the cleanest code but it's a start.

diff --git a/Makefile b/Makefile
index 78186f8..35a9118 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 PREFIX=/usr
 BINDIR=$(PREFIX)/bin
 CFLAGS=-Wall -g3
-gcodetool: main.o gcode.o mirror.o move.o info.o
+gcodetool: main.o gcode.o mirror.o move.o info.o preview.o preview_term.o preview_pnm.o
  $(CC) $^ -lm -o $@
 static: CC+=-static
 static: gcodetool
diff --git a/main.c b/main.c
index a82fb5a..e307c5c 100644
--- a/main.c
+++ b/main.c
@@ -22,6 +22,7 @@
 extern int gtool_mirror_arg(int argc, char** argv);
 extern int gtool_move_arg(int argc, char** argv);
 extern int gtool_info_arg(int argc, char** argv);
+extern int gtool_preview_arg(int argc, char** argv);
 
 int main(int argc, char** argv)
 {
@@ -37,6 +38,10 @@ int main(int argc, char** argv)
   {
     return gtool_info_arg(argc, argv);
   }
+  else if(argc>2 && !strcmp(argv[1], "preview"))
+  {
+    return gtool_preview_arg(argc, argv);
+  }
 #ifdef WIN32 // For lazy open-with
   else if(argc>1 && !access(argv[1], R_OK))
   {
diff --git a/preview.c b/preview.c
new file mode 100644
index 0000000..5bd393a
--- /dev/null
+++ b/preview.c
@@ -0,0 +1,273 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+#include "gcode.h"
+#include "preview.h"
+extern struct img* gtool_preview_term(int width, int height, const char* filename);
+extern struct img* gtool_preview_pnm(int width, int height, const char* filename);
+
+struct line // TODO: Consider non-planar printing?
+{
+  double x[2];
+  double y[2];
+};
+
+struct vectorlayer
+{
+  struct line* lines;
+  unsigned int linecount;
+  double z;
+};
+
+struct vectors
+{
+  struct vectorlayer* layers;
+  unsigned int layercount;
+  unsigned int current;
+};
+
+static void vector_draw(struct vectors* v, double startx, double starty, double endx, double endy, double z)
+{
+  if(v->layers && v->layers[v->current].z!=z)
+  {
+    for(v->current=0; v->current<v->layercount; ++v->current)
+    {
+      if(v->layers[v->current].z==z){break;}
+    }
+  }
+  if(v->current==v->layercount)
+  {
+    v->layers=realloc(v->layers, sizeof(struct vectorlayer)*(++v->layercount));
+    v->layers[v->current].z=z;
+    v->layers[v->current].lines=0;
+    v->layers[v->current].linecount=0;
+  }
+  struct vectorlayer* vl=&v->layers[v->current];
+  vl->lines=realloc(vl->lines, sizeof(struct line)*(++vl->linecount));
+  struct line* l=&vl->lines[vl->linecount-1];
+  l->x[0]=startx;
+  l->x[1]=endx;
+  l->y[0]=starty;
+  l->y[1]=endy;
+}
+
+int gtool_preview(const char* filename, struct img* img, double angle, double zrot)
+{
+  struct gcode* gcode=gcode_read(filename);
+  if(!gcode){return -1;}
+  const struct gcommand* cmd;
+  char tty=isatty(fileno(stdout));
+  double oldx=0;
+  double oldy=0;
+  double oldz=0;
+  double olde=0;
+  struct vectors vectors={0,0,0};
+  double max[]={NAN,NAN,NAN};
+  double min[]={NAN,NAN,NAN};
+  // Loop through gcode and analyze commands
+  while((cmd=gcode_readcommand(gcode)))
+  {
+    // Show progress if outputting to a tty
+    if(tty){printf("%.2f%%\r", (double)gcode->pos*100/(double)gcode->size);}
+    if(cmd->key=='G' && cmd->num<=1)
+    {
+      double xval=gcommand_getparam(cmd, 'X');
+      double yval=gcommand_getparam(cmd, 'Y');
+      double zval=gcommand_getparam(cmd, 'Z');
+      double eval=gcommand_getparam(cmd, 'E');
+      char move=(!isnan(xval) || !isnan(yval));
+      if(isnan(xval)){xval=oldx;}
+      if(isnan(yval)){yval=oldy;}
+      if(isnan(zval)){zval=oldz;}
+      if(!isnan(eval) && eval>olde && move)
+      {
+        olde=eval;
+        vector_draw(&vectors, oldx, oldy, xval, yval, zval);
+      }
+      oldx=xval;
+      oldy=yval;
+      oldz=zval;
+      if(isnan(max[0]) || xval>max[0]){max[0]=xval;}
+      if(isnan(max[1]) || yval>max[1]){max[1]=yval;}
+      if(isnan(max[2]) || zval>max[2]){max[2]=zval;}
+      if(isnan(min[0]) || xval<min[0]){min[0]=xval;}
+      if(isnan(min[1]) || yval<min[1]){min[1]=yval;}
+      if(isnan(min[2]) || zval<min[2]){min[2]=zval;}
+    }
+  }
+  gcode_free(gcode);
+  double center[]={(min[0]+max[0])/2, (min[1]+max[1])/2};
+  // Figure out how to scale things
+  double scale=0; // max[0];
+  unsigned int i;
+  for(i=0; i<3; ++i)
+  {
+    double dist=fabs(max[i]-min[i]);
+    if(dist>scale){scale=dist;}
+  }
+  scale=(img->width>img->height)?(img->height/scale):(img->width/scale);
+  scale*=sin(45);
+// TODO: Intermediate bitmap for shading? (plus then the final bitmap can be navigated with less re-processing. but that would be pixely)
+  while(1)
+  {
+    if(fabs(angle)>=M_PI){angle=(angle>0)?-M_PI:M_PI;}
+    // Figure out pixels-per-mm to find out how much wider lines need to be on high-res displays/methods. lines should be 0.4mm assuming "standard" nozzles
+    int linescale=ceil(scale*0.4);
+    unsigned int i;
+// To center onto a non-0 center bitmap, subtract center[], add width/2 / height/2
+#define cx(n) ((n-center[0])*scale)
+#define cy(n) ((n-center[1])*scale)
+    for(i=0; i<vectors.layercount; ++i)
+    {
+// TODO: Could probably save a lot of time by not doing these switches and instead having a start, end, and step
+if(fabs(angle)>M_PI_2){i=vectors.layercount-1-i;}
+      unsigned char c=80+i*175/vectors.layercount;
+      unsigned int j;
+      for(j=0; j<vectors.layers[i].linecount; ++j)
+      {
+        struct line* oldl=&vectors.layers[i].lines[j];
+// TODO: Do this more cleanly
+struct line line;
+struct line* l=&line;
+l->y[0]=sin(zrot)*cy(oldl->y[0])+cos(zrot)*cx(oldl->x[0]);
+l->x[0]=sin(zrot+M_PI_2)*cy(oldl->y[0])+cos(zrot+M_PI_2)*cx(oldl->x[0]);
+l->y[1]=sin(zrot)*cy(oldl->y[1])+cx(cos(zrot)*oldl->x[1]);
+l->x[1]=sin(zrot+M_PI_2)*cy(oldl->y[1])+cos(zrot+M_PI_2)*cx(oldl->x[1]);
+l->y[0]=l->y[0]*cos(angle)+vectors.layers[i].z*scale*sin(angle);
+l->y[1]=l->y[1]*cos(angle)+vectors.layers[i].z*scale*sin(angle);
+#define polcmp(n) ((n[0]>=0&&n[1]>=0) || (n[0]<0&&n[1]<0))
+        // Skip lines that are just out of view (saves resources when zoomed in far)
+        if(fabs(l->x[0])>img->width/2 && fabs(l->x[1])>img->width/2 && polcmp(l->x)){continue;}
+        if(fabs(l->y[0])>img->height/2 && fabs(l->y[1])>img->height/2 && polcmp(l->y)){continue;}
+        double stepx;
+        double stepy;
+        double steps;
+        // Figure out whether to go x/y or y/x
+        if(fabs(l->x[0]-l->x[1])>fabs(l->y[0]-l->y[1]))
+        {
+          steps=fabs(l->x[0]-l->x[1]);
+          stepx=(l->x[1]>l->x[0])?1:-1;
+          stepy=(l->y[1]-l->y[0])/steps;
+        }else{
+          steps=fabs(l->y[0]-l->y[1]);
+          stepy=(l->y[1]>l->y[0])?1:-1;
+          stepx=(l->x[1]-l->x[0])/steps;
+        }
+// Expand the line perpendicular to the line's angle
+double lineangle=atan2(stepx,stepy);
+double linestepx=sin(lineangle+M_PI_2);
+double linestepy=cos(lineangle+M_PI_2);
+int m;
+for(m=0; m<linescale; ++m)
+{
+        double k;
+        for(k=0; k<steps; ++k)
+        {
+          unsigned int x=(l->x[0]+img->width/2)+stepx*k+linestepx*(m-linescale/2);
+          unsigned int y=(l->y[0]+img->height/2)+stepy*k+linestepy*(m-linescale/2);
+          if(x>=img->width || y>=img->height){continue;}
+          img->drawpixel(img, x, y, c, c/1.05, c/2);
+        }
+      }
+}
+      if(fabs(angle)>M_PI_2){i=vectors.layercount-1-i;}
+    }
+    img->display(img);
+    img->clear(img);
+    if(img->input)
+    {
+      char quit=0;
+      switch(img->input(img))
+      {
+      case PREVIEW_XRP: angle+=M_PI/18; break;
+      case PREVIEW_XRM: angle-=M_PI/18; break;
+      case PREVIEW_ZRP: zrot+=M_PI/18; break;
+      case PREVIEW_ZRM: zrot-=M_PI/18; break;
+      case PREVIEW_ZOOMP: scale*=1.5; break;
+      case PREVIEW_ZOOMM: scale/=1.5; break;
+      case PREVIEW_QUIT: quit=1; break;
+      case PREVIEW_NOINPUT: break; // TODO: Somehow restart waiting for input. Don't bother redrawing the same perspective
+      }
+      if(quit){break;}
+    }else{break;}
+  }
+  if(img->clean){img->clean(img);}
+  return 0;
+}
+
+int gtool_preview_arg(int argc, char** argv)
+{
+  const char* filename=0;
+  const char* outfile=0;
+  double rx=0,rz=0;
+  int width=0,height=0;
+  struct img*(*imginit)(int width, int height, const char* filename)=0;
+  int i;
+  for(i=2; i<argc; ++i)
+  {
+    if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
+    {
+      filename=0; // Setting filename to 0 is a shortcut to avoid having to track the help option, both cases show the help
+      break;
+    }
+    else if(!strcmp(argv[i], "--rx") && i+1<argc)
+    {
+      rx=strtod(argv[++i],0)*M_PI/180;
+    }
+    else if(!strcmp(argv[i], "--rz") && i+1<argc)
+    {
+      rz=strtod(argv[++i],0)*M_PI/180;
+    }
+    else if((!strcmp(argv[i], "-o") || !strcmp(argv[i], "--output")) && i+2<argc)
+    {
+      if(!strcmp(argv[i+1], "term")){imginit=gtool_preview_term;}
+      else if(!strcmp(argv[i+1], "pnm")){imginit=gtool_preview_pnm;}
+      i+=2;
+      outfile=argv[i];
+    }
+    else if((!strcmp(argv[i], "-s") || !strcmp(argv[i], "--size")) && i+1<argc)
+    {
+      char* end=0;
+      width=strtol(argv[++i], &end, 0);
+      if(end && end[0]){height=strtol(&end[1], 0, 0);}else{height=width;}
+    }
+    else if(!filename)
+    {
+      filename=argv[i];
+    }
+  }
+  if(!filename)
+  {
+    printf("Usage: %s %s [options] <filename>\n"
+           "Options:\n"
+           "-h/--help                   = display this help text\n"
+           "--r[xz] <angle>             = rotate X or Z <angle> degrees\n"
+           "-o/--output <method> <file> = select output method and filename\n"
+           "-s/--size <width>x<height>  = set output size\n"
+           "TODO: scale etc.\n", argv[0], argv[1]);
+    return -1;
+  }
+// TODO: Mechanism for selecting method by what's available. Not relevant until there's another direct display method
+  if(!imginit){imginit=gtool_preview_term;}
+  struct img* img=imginit(width,height,outfile);
+  return gtool_preview(filename, img, rx, rz);
+}
diff --git a/preview.h b/preview.h
new file mode 100644
index 0000000..4e664eb
--- /dev/null
+++ b/preview.h
@@ -0,0 +1,40 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+enum previewcontrol
+{
+  PREVIEW_XRP=0,
+  PREVIEW_XRM,
+  PREVIEW_ZRP,
+  PREVIEW_ZRM,
+// TODO: Control Y as well? And translation?
+  PREVIEW_ZOOMP,
+  PREVIEW_ZOOMM,
+  PREVIEW_QUIT,
+  PREVIEW_NOINPUT
+};
+
+struct img
+{
+  int width;
+  int height;
+  void(*drawpixel)(struct img*,int x, int y, unsigned char r, unsigned char g, unsigned char b);
+  void(*clear)(struct img*);
+  void(*display)(struct img*);
+  enum previewcontrol(*input)(struct img*);
+  void(*clean)(struct img*);
+};
diff --git a/preview_pnm.c b/preview_pnm.c
new file mode 100644
index 0000000..0893ffa
--- /dev/null
+++ b/preview_pnm.c
@@ -0,0 +1,98 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "preview.h"
+
+struct img_pnm
+{
+  struct img img;
+  unsigned char* buf;
+  FILE* out;
+  char bin;
+};
+
+static void cb_drawpixel(struct img* imgp,int x, int y, unsigned char r, unsigned char g, unsigned char b)
+{
+  struct img_pnm* img=(struct img_pnm*)imgp;
+  img->buf[(x+y*imgp->width)*3]=r;
+  img->buf[(x+y*imgp->width)*3+1]=g;
+  img->buf[(x+y*imgp->width)*3+2]=b;
+}
+
+static void cb_clear(struct img* imgp)
+{
+  struct img_pnm* img=(struct img_pnm*)imgp;
+  memset(img->buf, 0, img->img.width*img->img.height*3);
+}
+
+static void cb_display(struct img* imgp)
+{
+  struct img_pnm* img=(struct img_pnm*)imgp;
+  unsigned int i;
+  fprintf(img->out, "P%i\n# Generated by gcodetool <https://gcodetool.ion.nu/>\n%i %i\n255\n", 3+img->bin*3, imgp->width, imgp->height);
+  if(img->bin)
+  {
+    fwrite(img->buf, 3, imgp->width*imgp->height, img->out);
+  }else{
+    for(i=0; i<imgp->width*imgp->height*3; ++i)
+    {
+      fprintf(img->out, "%hhu ", img->buf[i]);
+// TODO: Would X/Y arithmetics be faster than this one conditional?
+//    if(!((i+1)%imgp->width)){write(img->fd, "\n", 1); i+=imgp->width;}
+    }
+  }
+}
+
+static void cb_clean(struct img* imgp)
+{
+  struct img_pnm* img=(struct img_pnm*)imgp;
+  free(img->buf);
+  free(img);
+}
+
+struct img* gtool_preview_pnm(int width, int height, const char* filename)
+{
+  struct img_pnm* img=malloc(sizeof(struct img_pnm));
+  if(filename && strcmp(filename, "-"))
+  {
+    img->out=fopen(filename, "w");
+    img->bin=1;
+  }else{
+    img->out=stdout;
+    img->bin=0;
+  }
+  if(!width || !height) // Make up sizes if not specified
+  {
+    if(!width){width=1024;}
+    if(!height){height=1024;}
+  }
+  // Allocate buffer
+  img->img.width=width;
+  img->img.height=height;
+  img->buf=malloc(width*height*3);
+  memset(img->buf, 0, width*height);
+  // Set callbacks
+  img->img.drawpixel=cb_drawpixel;
+  img->img.clear=cb_clear;
+  img->img.display=cb_display;
+  img->img.input=0;
+  img->img.clean=cb_clean;
+  return (struct img*)img;
+}
diff --git a/preview_term.c b/preview_term.c
new file mode 100644
index 0000000..76e8c8b
--- /dev/null
+++ b/preview_term.c
@@ -0,0 +1,144 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+#include "preview.h"
+
+struct img_term
+{
+  struct img img;
+  unsigned char* buf;
+  struct termios term_orig;
+  int fd;
+};
+
+static void termcolor(int fd, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2)
+{
+// 24bit, TODO: make this togglable somehow
+/*
+    printf("\e[38;2;%hhu;%hhu;%hhum"
+           "\e[48;2;%hhu;%hhu;%hhum"
+           "\u2580" // 9600
+           "\e[0m", r1, g1, b1, r2, g2, b2);
+*/
+// 8bit
+  unsigned int top=232+r1/11;
+  unsigned int bottom=232+r2/11;
+  dprintf(fd,
+    "\e[38;5;%um"
+    "\e[48;5;%um"
+    "\u2580" // 9600
+    "\e[0m", top, bottom);
+}
+
+static void cb_drawpixel(struct img* imgp,int x, int y, unsigned char r, unsigned char g, unsigned char b)
+{
+  struct img_term* img=(struct img_term*)imgp;
+  img->buf[x+y*img->img.width]=r/3+g/3+b/3; // TODO: handle >8 bits
+}
+
+static void cb_clear(struct img* imgp)
+{
+  struct img_term* img=(struct img_term*)imgp;
+  memset(img->buf, 0, img->img.width*img->img.height); // TODO: handle >8 bits
+}
+
+static void cb_display(struct img* imgp)
+{
+  struct img_term* img=(struct img_term*)imgp;
+  write(img->fd, "\e[H", 3);
+  unsigned char* buf=img->buf;
+  unsigned int i;
+  for(i=0; i<imgp->width*(imgp->height-1); ++i)
+  {
+    termcolor(img->fd, buf[i], buf[i], buf[i], buf[i+imgp->width], buf[i+imgp->width], buf[i+imgp->width]);
+// TODO: Would X/Y arithmetics be faster than this one conditional?
+    if(!((i+1)%imgp->width) && i+1<imgp->width*(imgp->height-1)){write(img->fd, "\n", 1); i+=imgp->width;}
+  }
+}
+
+static enum previewcontrol cb_input(struct img* imgp)
+{
+  char key[10];
+  int keylen;
+  if((keylen=read(0, key, 9))>0)
+  {
+    if(keylen==3 && key[0]=='\e' && key[1]=='[')
+    {
+      return key[2]-'A';
+    }
+    if(keylen==1)
+    {
+      if(key[0]=='q'){return PREVIEW_QUIT;}
+      if(key[0]=='+'){return PREVIEW_ZOOMP;}
+      if(key[0]=='-'){return PREVIEW_ZOOMM;}
+    }
+  }
+  return PREVIEW_NOINPUT;
+}
+
+static void cb_clean(struct img* imgp)
+{
+  struct img_term* img=(struct img_term*)imgp;
+  tcsetattr(0,TCSANOW,&img->term_orig);
+  free(img->buf);
+  free(img);
+}
+
+struct img* gtool_preview_term(int width, int height, const char* filename)
+{
+  struct img_term* img=malloc(sizeof(struct img_term));
+  if(filename && strcmp(filename, "-"))
+  {
+    img->fd=open(filename, O_WRONLY|O_TRUNC);
+  }else{
+    img->fd=1;
+  }
+  if(!width || !height) // Get screen size
+  {
+    struct winsize ws;
+    ioctl(1, TIOCGWINSZ, &ws);
+// TODO: Handle failure to get terminal size, will happen for non '-' filenames
+    if(!width){width=ws.ws_col;}
+    if(!height){height=ws.ws_row*2;} // 2 pixels per row thanks to half solid symbol
+// printf("Screen size: %ix%i\n", ws.ws_row, ws.ws_col);
+  }
+  // Allocate buffer
+  img->img.width=width;
+  img->img.height=height;
+  img->buf=malloc(width*height); // TODO: Handle >8 bit as well
+  memset(img->buf, 0, width*height);
+  // Set terminal mode to non-canonical (not line-buffered) and without echo
+  struct termios term;
+  tcgetattr(0,&term);
+  tcgetattr(0,&img->term_orig);
+  term.c_lflag^=ICANON|ECHO;
+  tcsetattr(0,TCSANOW,&term);
+  // Set callbacks
+  img->img.drawpixel=cb_drawpixel;
+  img->img.clear=cb_clear;
+  img->img.display=cb_display;
+  img->img.input=cb_input;
+  img->img.clean=cb_clean;
+  return (struct img*)img;
+}