/* grid.cc -- code for drawing grid view.  -*- C++ -*-
   Copyright (C) 2025-2026 Gerardo Ballabio

   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 <http://www.gnu.org/licenses/>. */

#include <chrono>
#include <fstream>
#include <functional>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <gtkmm.h>
#include <epoxy/gl.h>
#include "grid.h"

const GLuint restart = static_cast<GLuint>(-1);

bool gl_grid::compute_grid()
{
  refresh_grid = refresh_values = refresh_indices = refresh_surfaces =
    refresh_vbuffer = refresh_ibuffer = false;

  // ensure these doesn't change while computing
  GLuint size_[3];
  size_[0] = new_size[0];
  size_[1] = new_size[1];
  size_[2] = new_size[2];

  // set dimensions
  n_vertices = 4 * // three spatial coordinates + one value
    (size_[0] + 1) * (size_[1] + 1) * (size_[2] + 1); // [0, size_] included
  vertices.resize(n_vertices);

  unsigned n = nthreads; // ensure it doesn't change while computing
  if (n > 1)
    {
      std::vector<std::thread *> subworker(n);
      // start subthreads
      for (unsigned m=0; m<n; ++m)
	subworker[m] = new std::thread(&gl_grid::compute_grid_sub, this,
				       size_, m, n);
      // wait for completion
      for (unsigned m=0; m<n; ++m)
	subworker[m]->join();
      // clean up
      for (unsigned m=0; m<n; ++m)
	delete subworker[m];
    }
  else
    compute_grid_sub(size_, 0, 1); // all in one go

  if (stop_worker)
    return false; // terminate with no effect

  return true;
}

void gl_grid::compute_grid_sub(const GLuint size_[3], unsigned m, unsigned n)
{
  // m-th subthread of n

  // build (a slice of) vertex array
  // terminate loop if stop_worker is set
  GLdouble max = 1.0 + scale_margin, min = -max;
  for (unsigned k=m, l=4*(size_[1]+1)*(size_[0]+1)*m; // m-th plane every n
       k<=size_[2] && !stop_worker;
       k+=n, l+=4*(size_[1]+1)*(size_[0]+1)*(n-1)) // skip n-1 planes
    for (unsigned j=0; j<=size_[1] && !stop_worker; ++j)
      for (unsigned i=0; i<=size_[0] && !stop_worker; ++i)
	{
	  // grid points
	  GLdouble x = min + i * (max - min) / size_[0],
	    y = min + j * (max - min) / size_[1],
	    z = min + k * (max - min) / size_[2];
	  vertices[l++] = x;
	  vertices[l++] = y;
	  vertices[l++] = z;
	  vertices[l++] = 0.0;
	}
}

bool gl_grid::compute_values()
{
  refresh_values = refresh_surfaces = refresh_vbuffer = false;

  if (!f)
    return true;

  // ensure these don't change while computing
  GLuint size_[3];
  size_[0] = new_size[0];
  size_[1] = new_size[1];
  size_[2] = new_size[2];
  unsigned n = nthreads;

  if (!fix_vmax)
    new_vmax = 0.0;
  if (n > 1)
    {
      std::vector<std::thread *> subworker(n);
      // start subthreads
      for (unsigned m=0; m<n; ++m)
	subworker[m] = new std::thread(&gl_grid::compute_values_sub, this,
				       size_, m, n);
      // wait for completion
      for (unsigned m=0; m<n; ++m)
	subworker[m]->join();
      // clean up
      for (unsigned m=0; m<n; ++m)
	delete subworker[m];
    }
  else
    compute_values_sub(size_, 0, 1); // all in one go
  if (!fix_vmax && new_vmax == 0.0)
    new_vmax = 1.0;

  if (stop_worker)
    return false; // terminate with no effect

  return true;
}

void gl_grid::compute_values_sub(const GLuint size_[3],
				 unsigned m, unsigned n)
{
  // m-th subthread of n

  // build vertex array
  // terminate loop if stop_worker is set
  GLdouble max = new_scale * (1.0 + scale_margin), min = -max;
  double vmax_m = 0.0;
  for (unsigned k=m, l=4*(size_[1]+1)*(size_[0]+1)*m; // m-th plane every n
       k<=size_[2] && !stop_worker;
       k+=n, l+=4*(size_[1]+1)*(size_[0]+1)*(n-1)) // skip n-1 planes
    for (unsigned j=0; j<=size_[1] && !stop_worker; ++j)
      for (unsigned i=0; i<=size_[0] && !stop_worker; ++i)
	{
	  // grid points
	  GLdouble x = min + i * (max - min) / size_[0],
	    y = min + j * (max - min) / size_[1],
	    z = min + k * (max - min) / size_[2];
	  // function value
	  GLdouble v = f(x, y, z);
	  if (v > vmax_m)
	    vmax_m = v;
	  else if (v < -vmax_m)
	    vmax_m = -v;
	  vertices[l+3] = v;
	  l += 4;
	}
  if (!fix_vmax && vmax_m > new_vmax)
    new_vmax = vmax_m;
}

bool gl_grid::compute_indices()
{
  refresh_indices = refresh_surfaces = refresh_ibuffer = false;

  // ensure these don't change while computing
  GLuint size_[3], step1_[3], step2_[3];
  size_[0] = new_size[0];
  size_[1] = new_size[1];
  size_[2] = new_size[2];
  step1_[0] = new_step1[0];
  step1_[1] = new_step1[1];
  step1_[2] = new_step1[2];
  step2_[0] = new_step2[0];
  step2_[1] = new_step2[1];
  step2_[2] = new_step2[2];
  unsigned n = nthreads;

  // build index arrays
  // terminate loops if stop_worker is set
  new_n_planes[0] = size_[0] / step2_[0] + 1; // number of planes
  new_n_planes[1] = size_[1] / step2_[1] + 1;
  new_n_planes[2] = size_[2] / step2_[2] + 1;
  new_n_stride[0] = (size_[1] / step1_[1]) * // number of strips
    (size_[2] / step1_[2] * 2 + 3); // no. of points per strip + 1 for restart
  new_n_stride[1] = (size_[2] / step1_[2]) * (size_[0] / step1_[0] * 2 + 3);
  new_n_stride[2] = (size_[0] / step1_[0]) * (size_[1] / step1_[1] * 2 + 3);
  new_n_indices[0] = new_n_planes[0] * new_n_stride[0];
  new_n_indices[1] = new_n_planes[1] * new_n_stride[1] + new_n_indices[0];
  new_n_indices[2] = new_n_planes[2] * new_n_stride[2] + new_n_indices[1];
  indices.resize(new_n_indices[2]);

  if (n > 1)
    {
      std::vector<std::thread *> subworker(n);
      // start subthreads
      for (unsigned m=0; m<n; ++m)
	subworker[m] = new std::thread(&gl_grid::compute_indices_sub, this,
				       size_, step1_, step2_, m, n);
      // wait for completion
      for (unsigned m=0; m<n; ++m)
	subworker[m]->join();
      // clean up
      for (unsigned m=0; m<n; ++m)
	delete subworker[m];
    }
  else
    compute_indices_sub(size_, step1_, step2_, 0, 1); // all in one go

  if (stop_worker)
    return false; // terminate with no effect

  return true;
}

void gl_grid::compute_indices_sub
(const GLuint size_[3], const GLuint step1_[3], const GLuint step2_[3],
 unsigned m, unsigned n)
{
  // m-th subthread of n

  // planes orthogonal to x axis, increasing values of x
  for (unsigned i=step2_[0]*m, l=new_n_stride[0]*m; // m-th plane every n
       i<=size_[0] && !stop_worker; i+=step2_[0]*n, l+=new_n_stride[0]*(n-1))
    for (unsigned j=0; j<=size_[1]-step1_[1] && !stop_worker; j+=step1_[1])
      {
	for (unsigned k=0; k<=size_[2] && !stop_worker; k+=step1_[2])
	  {
	    indices[l++] = n_bar / 4 +
	      i + (size_[0] + 1) * (j + (size_[1] + 1) * k);
	    indices[l++] = n_bar / 4 +
	      i + (size_[0] + 1) * ((j + step1_[1]) + (size_[1] + 1) * k);
	  }
	indices[l++] = restart;
      }

  // planes orthogonal to y axis, increasing values of y
  for (unsigned j=step2_[1]*m, l=new_n_indices[0]+new_n_stride[1]*m;
       j<=size_[1] && !stop_worker; j+=step2_[1]*n, l+=new_n_stride[1]*(n-1))
    for (unsigned k=0; k<=size_[2]-step1_[2] && !stop_worker; k+=step1_[2])
      {
	for (unsigned i=0; i<=size_[0] && !stop_worker; i+=step1_[0])
	  {
	    indices[l++] = n_bar / 4 +
	      i + (size_[0] + 1) * (j + (size_[1] + 1) * k);
	    indices[l++] = n_bar / 4 +
	      i + (size_[0] + 1) * (j + (size_[1] + 1) * (k + step1_[2]));
	  }
	indices[l++] = restart;
      }

  // planes orthogonal to z axis, increasing values of z
  for (unsigned k=step2_[2]*m, l=new_n_indices[1]+new_n_stride[2]*m;
       k<=size_[2] && !stop_worker; k+=step2_[2]*n, l+=new_n_stride[2]*(n-1))
    for (unsigned i=0; i<=size_[0]-step1_[0] && !stop_worker; i+=step1_[0])
      {
	for (unsigned j=0; j<=size_[1] && !stop_worker; j+=step1_[1])
	  {
	    indices[l++] = n_bar / 4 +
	      i + (size_[0] + 1) * (j + (size_[1] + 1) * k);
	    indices[l++] = n_bar / 4 +
	      (i + step1_[0]) + (size_[0] + 1) * (j + (size_[1] + 1) * k);
	  }
	indices[l++] = restart;
      }
}

bool gl_grid::compute_surfaces()
{
  refresh_surfaces = refresh_vbuffer = refresh_ibuffer = false;

  // ensure these don't change while computing
  GLuint size_[3], step1_[3], step2_[3];
  size_[0] = new_size[0];
  size_[1] = new_size[1];
  size_[2] = new_size[2];
  step1_[0] = new_step1[0];
  step1_[1] = new_step1[1];
  step1_[2] = new_step1[2];
  step2_[0] = new_step2[0];
  step2_[1] = new_step2[1];
  step2_[2] = new_step2[2];
  GLuint n_surfaces_ = new_n_surfaces, n_indices_[3];
  n_indices_[0] = new_n_indices[0];
  n_indices_[1] = new_n_indices[1];
  n_indices_[2] = new_n_indices[2];
  unsigned n = nthreads;

  // build surfaces
  // terminate loops if stop_worker is set
  surfaces.resize(0);
  indices_s.resize(0);
  new_indices_i.resize(0);
  new_indices_i.push_back(n_indices_[2]);
  surfaces_step = 0;

  if (n > 1)
    {
      std::vector<std::thread *> subworker(n);
      // start subthreads
      for (unsigned m=0; m<n; ++m)
	subworker[m] = new std::thread(&gl_grid::compute_surfaces_sub, this,
				       size_, step1_, step2_,
				       n_indices_, n_surfaces_, m, n);
      // wait for completion
      for (unsigned m=0; m<n; ++m)
	subworker[m]->join();
      // clean up
      for (unsigned m=0; m<n; ++m)
	delete subworker[m];
    }
  else
    compute_surfaces_sub(size_, step1_, step2_, n_indices_, n_surfaces_,
			 0, 1); // all in one go

  if (stop_worker)
    return false; // terminate with no effect

  return true;
}

void gl_grid::compute_surfaces_sub
(const GLuint size_[3], const GLuint step1_[3], const GLuint step2_[3],
 const GLuint n_indices_[3], GLuint n_surfaces_, unsigned m, unsigned n)
{
  // m-th subthread of n

  GLfloat base = -new_vmax / new_vscale * (1.0 + 1.0 / n_surfaces_),
    step = new_vmax / new_vscale * 2.0 / n_surfaces_;
  // vertices of a cube of adjacent grid points
  GLuint p[8];
  int s[8];
  int min, max, min0, max0, min1, max1;

  std::vector<GLfloat> sub_surfaces;
  std::vector<GLuint> sub_indices_i, sub_indices_s;
  std::vector<GLuint>::size_type i_offset, s_offset;
  GLuint l_offset;

  // planes orthogonal to x axis, increasing values of x
  for (unsigned i=size_[0]*m/(step2_[0]*n)*step2_[0], l=0;
       i<=size_[0]-step2_[0] && i<size_[0]*(m+1)/(step2_[0]*n)*step2_[0] &&
	 !stop_worker;
       i+=step2_[0])
    {
      for (unsigned j=0; j<=size_[1]-step1_[1] && !stop_worker; j+=step1_[1])
	{
	  // front face of first cube
	  // grid points
	  p[0] = 3 + 4 * (i + (size_[0] + 1) * j);
	  p[1] = p[0] + 4 * step2_[0];
	  p[2] = p[0] + 4 * step1_[1] * (size_[0] + 1);
	  p[3] = p[2] + 4 * step2_[0];
	  // surface levels of points
	  s[0] = std::floor((vertices[p[0]] - base) / step);
	  s[1] = std::floor((vertices[p[1]] - base) / step);
	  s[2] = std::floor((vertices[p[2]] - base) / step);
	  s[3] = std::floor((vertices[p[3]] - base) / step);
	  // minimum and maximum level (clamped to [0, n_surfaces_])
	  min0 = (s[0] < s[1]) ? s[0] : s[1];
	  min0 = (min0 < s[2]) ? min0 : s[2];
	  min0 = (min0 < s[3]) ? min0 : s[3];
	  min0 = (min0 < 0) ? 0 : min0;
	  min0 = (min0 > n_surfaces_) ? n_surfaces_ : min0;
	  max0 = (s[0] > s[1]) ? s[0] : s[1];
	  max0 = (max0 > s[2]) ? max0 : s[2];
	  max0 = (max0 > s[3]) ? max0 : s[3];
	  max0 = (max0 < 0) ? 0 : max0;
	  max0 = (max0 > n_surfaces_) ? n_surfaces_ : max0;
	  for (unsigned k=step1_[2]; k<=size_[2] && !stop_worker;
	       k+=step1_[2])
	    {
	      // back face of current cube
	      // grid points
	      p[4] = p[0] + 4 * step1_[2] * (size_[0] + 1) * (size_[1] + 1);
	      p[5] = p[1] + 4 * step1_[2] * (size_[0] + 1) * (size_[1] + 1);
	      p[6] = p[2] + 4 * step1_[2] * (size_[0] + 1) * (size_[1] + 1);
	      p[7] = p[3] + 4 * step1_[2] * (size_[0] + 1) * (size_[1] + 1);
	      // surface levels of points
	      s[4] = std::floor((vertices[p[4]] - base) / step);
	      s[5] = std::floor((vertices[p[5]] - base) / step);
	      s[6] = std::floor((vertices[p[6]] - base) / step);
	      s[7] = std::floor((vertices[p[7]] - base) / step);
	      // minimum and maximum level (clamped to [0, n_surfaces_])
	      min1 = (s[4] < s[5]) ? s[4] : s[5];
	      min1 = (min1 < s[6]) ? min1 : s[6];
	      min1 = (min1 < s[7]) ? min1 : s[7];
	      min1 = (min1 < 0) ? 0 : min1;
	      min1 = (min1 > n_surfaces_) ? n_surfaces_ : min1;
	      max1 = (s[4] > s[5]) ? s[4] : s[5];
	      max1 = (max1 > s[6]) ? max1 : s[6];
	      max1 = (max1 > s[7]) ? max1 : s[7];
	      max1 = (max1 < 0) ? 0 : max1;
	      max1 = (max1 > n_surfaces_) ? n_surfaces_ : max1;
	      min = (min0 < min1) ? min0 : min1;
	      max = (max0 > max1) ? max0 : max1;
	      // if min == max then no surface passes through this cube
	      if (min < max)
		compute_surfaces_cube(sub_surfaces, sub_indices_s,
				      l, p, s, min, max, base, step);
	      // back face of this cube is front face of next cube
	      p[0] = p[4];
	      p[1] = p[5];
	      p[2] = p[6];
	      p[3] = p[7];
	      s[0] = s[4];
	      s[1] = s[5];
	      s[2] = s[6];
	      s[3] = s[7];
	      min0 = min1;
	      max0 = max1;
	    }
	}
      sub_indices_i.push_back(sub_indices_s.size());
    }

  // subthreads must append to vectors in sequence

  // wait for previous subthread to finish
  for (; surfaces_step<m && !stop_worker; )
    // do something inside the loop (even if it doesn't actually do anything)
    // or it will be optimized away
    // also, don't waste cycles
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  if (!stop_worker)
    {
      // restart counting where previous subthread ended
      i_offset = new_indices_i.size();
      s_offset = indices_s.size();
      if (indices_s.size() > 1)
	l_offset = indices_s[indices_s.size() - 2] + 1; // last is restart
      else
	l_offset = (n_bar + n_vertices) / 4;
      // copy vectors with appropriate offsets
      // free temporary memory as soon as possible
      surfaces.insert(surfaces.end(),
		      sub_surfaces.begin(), sub_surfaces.end());
      sub_surfaces.resize(0);
      indices_s.resize(s_offset + sub_indices_s.size());
      for (std::vector<GLuint>::iterator src=sub_indices_s.begin(),
	     dest=indices_s.begin()+s_offset; src!=sub_indices_s.end();
	   ++src, ++dest)
	*dest = (*src != restart) ? (*src + l_offset) : restart;
      sub_indices_s.resize(0);
      new_indices_i.resize(i_offset + sub_indices_i.size());
      for (std::vector<GLuint>::iterator src=sub_indices_i.begin(),
	     dest=new_indices_i.begin()+i_offset; src!=sub_indices_i.end();
	   ++src, ++dest)
	*dest = *src + n_indices_[2] + s_offset;
      sub_indices_i.resize(0);
    }
  // unblock next subthread
  ++surfaces_step;

  // planes orthogonal to y axis, increasing values of y
  for (unsigned j=size_[1]*m/(step2_[1]*n)*step2_[1], l=0;
       j<=size_[1]-step2_[1] && j<size_[1]*(m+1)/(step2_[1]*n)*step2_[1] &&
	 !stop_worker;
       j+=step2_[1])
    {
      for (unsigned k=0; k<=size_[2]-step1_[2] && !stop_worker; k+=step1_[2])
	{
	  // front face of first cube
	  // grid points
	  p[0] = 3 + 4 * (size_[0] + 1) * (j + (size_[1] + 1) * k);
	  p[2] = p[0] + 4 * step2_[1] * (size_[0] + 1);
	  p[4] = p[0] + 4 * step1_[2] * (size_[0] + 1) * (size_[1] + 1);
	  p[6] = p[4] + 4 * step2_[1] * (size_[0] + 1);
	  // surface levels of points
	  s[0] = std::floor((vertices[p[0]] - base) / step);
	  s[2] = std::floor((vertices[p[2]] - base) / step);
	  s[4] = std::floor((vertices[p[4]] - base) / step);
	  s[6] = std::floor((vertices[p[6]] - base) / step);
	  // minimum and maximum level (clamped to [0, n_surfaces_])
	  min0 = (s[0] < s[2]) ? s[0] : s[2];
	  min0 = (min0 < s[4]) ? min0 : s[4];
	  min0 = (min0 < s[6]) ? min0 : s[6];
	  min0 = (min0 < 0) ? 0 : min0;
	  min0 = (min0 > n_surfaces_) ? n_surfaces_ : min0;
	  max0 = (s[0] > s[2]) ? s[0] : s[2];
	  max0 = (max0 > s[4]) ? max0 : s[4];
	  max0 = (max0 > s[6]) ? max0 : s[6];
	  max0 = (max0 < 0) ? 0 : max0;
	  max0 = (max0 > n_surfaces_) ? n_surfaces_ : max0;
	  for (unsigned i=step1_[0]; i<=size_[0] && !stop_worker;
	       i+=step1_[0])
	    {
	      // back face of current cube
	      // grid points
	      p[1] = p[0] + 4 * step1_[0];
	      p[3] = p[2] + 4 * step1_[0];
	      p[5] = p[4] + 4 * step1_[0];
	      p[7] = p[6] + 4 * step1_[0];
	      // surface levels of points
	      s[1] = std::floor((vertices[p[1]] - base) / step);
	      s[3] = std::floor((vertices[p[3]] - base) / step);
	      s[5] = std::floor((vertices[p[5]] - base) / step);
	      s[7] = std::floor((vertices[p[7]] - base) / step);
	      // minimum and maximum level (clamped to [0, n_surfaces_])
	      min1 = (s[1] < s[3]) ? s[1] : s[3];
	      min1 = (min1 < s[5]) ? min1 : s[5];
	      min1 = (min1 < s[7]) ? min1 : s[7];
	      min1 = (min1 < 0) ? 0 : min1;
	      min1 = (min1 > n_surfaces_) ? n_surfaces_ : min1;
	      max1 = (s[1] > s[3]) ? s[1] : s[3];
	      max1 = (max1 > s[5]) ? max1 : s[5];
	      max1 = (max1 > s[7]) ? max1 : s[7];
	      max1 = (max1 < 0) ? 0 : max1;
	      max1 = (max1 > n_surfaces_) ? n_surfaces_ : max1;
	      min = (min0 < min1) ? min0 : min1;
	      max = (max0 > max1) ? max0 : max1;
	      // if min == max then no surface passes through this cube
	      if (min < max)
		compute_surfaces_cube(sub_surfaces, sub_indices_s,
				      l, p, s, min, max, base, step);
	      // back face of this cube is front face of next cube
	      p[0] = p[1];
	      p[2] = p[3];
	      p[4] = p[5];
	      p[6] = p[7];
	      s[0] = s[1];
	      s[2] = s[3];
	      s[4] = s[5];
	      s[6] = s[7];
	      min0 = min1;
	      max0 = max1;
	    }
	}
      sub_indices_i.push_back(sub_indices_s.size());
    }

  // subthreads must append to vectors in sequence

  // wait for previous subthread to finish
  for (; surfaces_step<m+n && !stop_worker; )
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  if (!stop_worker)
    {
      // restart counting where previous subthread ended
      i_offset = new_indices_i.size();
      s_offset = indices_s.size();
      if (indices_s.size() > 1)
	l_offset = indices_s[indices_s.size() - 2] + 1; // last is restart
      else
	l_offset = (n_bar + n_vertices) / 4;
      // copy vectors with appropriate offsets
      surfaces.insert(surfaces.end(),
		      sub_surfaces.begin(), sub_surfaces.end());
      sub_surfaces.resize(0);
      indices_s.resize(s_offset + sub_indices_s.size());
      for (std::vector<GLuint>::iterator src=sub_indices_s.begin(),
	     dest=indices_s.begin()+s_offset; src!=sub_indices_s.end();
	   ++src, ++dest)
	*dest = (*src != restart) ? (*src + l_offset) : restart;
      sub_indices_s.resize(0);
      new_indices_i.resize(i_offset + sub_indices_i.size());
      for (std::vector<GLuint>::iterator src=sub_indices_i.begin(),
	     dest=new_indices_i.begin()+i_offset; src!=sub_indices_i.end();
	   ++src, ++dest)
	*dest = *src + n_indices_[2] + s_offset;
      sub_indices_i.resize(0);
    }
  // unblock next subthread
  ++surfaces_step;

  // planes orthogonal to z axis, increasing values of z
  for (unsigned k=size_[2]*m/(step2_[2]*n)*step2_[2], l=0;
       k<=size_[2]-step2_[2] && k<size_[2]*(m+1)/(step2_[2]*n)*step2_[2] &&
	 !stop_worker;
       k+=step2_[2])
    {
      for (unsigned i=0; i<=size_[0]-step1_[0] && !stop_worker; i+=step1_[0])
	{
	  // front face of first cube
	  // grid points
	  p[0] = 3 + 4 * (i + (size_[0] + 1) * (size_[1] + 1) * k);
	  p[1] = p[0] + 4 * step1_[0];
	  p[4] = p[0] + 4 * step2_[2] * (size_[0] + 1) * (size_[1] * 1);
	  p[5] = p[4] + 4 * step1_[0];
	  // surface levels of points
	  s[0] = std::floor((vertices[p[0]] - base) / step);
	  s[1] = std::floor((vertices[p[1]] - base) / step);
	  s[4] = std::floor((vertices[p[4]] - base) / step);
	  s[5] = std::floor((vertices[p[5]] - base) / step);
	  // minimum and maximum level (clamped to [0, n_surfaces_])
	  min0 = (s[0] < s[1]) ? s[0] : s[1];
	  min0 = (min0 < s[4]) ? min0 : s[4];
	  min0 = (min0 < s[5]) ? min0 : s[5];
	  min0 = (min0 < 0) ? 0 : min0;
	  min0 = (min0 > n_surfaces_) ? n_surfaces_ : min0;
	  max0 = (s[0] > s[1]) ? s[0] : s[1];
	  max0 = (max0 > s[4]) ? max0 : s[4];
	  max0 = (max0 > s[5]) ? max0 : s[5];
	  max0 = (max0 < 0) ? 0 : max0;
	  max0 = (max0 > n_surfaces_) ? n_surfaces_ : max0;
	  for (unsigned j=step1_[1]; j<=size_[1] && !stop_worker;
	       j+=step1_[1])
	    {
	      // back face of current cube
	      // grid points
	      p[2] = p[0] + 4 * step1_[1] * (size_[0] + 1);
	      p[3] = p[1] + 4 * step1_[1] * (size_[0] + 1);
	      p[6] = p[4] + 4 * step1_[1] * (size_[0] + 1);
	      p[7] = p[5] + 4 * step1_[1] * (size_[0] + 1);
	      // surface levels of points
	      s[2] = std::floor((vertices[p[2]] - base) / step);
	      s[3] = std::floor((vertices[p[3]] - base) / step);
	      s[6] = std::floor((vertices[p[6]] - base) / step);
	      s[7] = std::floor((vertices[p[7]] - base) / step);
	      // minimum and maximum level (clamped to [0, n_surfaces_])
	      min1 = (s[2] < s[3]) ? s[2] : s[3];
	      min1 = (min1 < s[6]) ? min1 : s[6];
	      min1 = (min1 < s[7]) ? min1 : s[7];
	      min1 = (min1 < 0) ? 0 : min1;
	      min1 = (min1 > n_surfaces_) ? n_surfaces_ : min1;
	      max1 = (s[2] > s[3]) ? s[2] : s[3];
	      max1 = (max1 > s[6]) ? max1 : s[6];
	      max1 = (max1 > s[7]) ? max1 : s[7];
	      max1 = (max1 < 0) ? 0 : max1;
	      max1 = (max1 > n_surfaces_) ? n_surfaces_ : max1;
	      min = (min0 < min1) ? min0 : min1;
	      max = (max0 > max1) ? max0 : max1;
	      // if min == max then no surface passes through this cube
	      if (min < max)
		compute_surfaces_cube(sub_surfaces, sub_indices_s,
				      l, p, s, min, max, base, step);
	      // back face of this cube is front face of next cube
	      p[0] = p[2];
	      p[1] = p[3];
	      p[4] = p[6];
	      p[5] = p[7];
	      s[0] = s[2];
	      s[1] = s[3];
	      s[4] = s[6];
	      s[5] = s[7];
	      min0 = min1;
	      max0 = max1;
	    }
	}
      sub_indices_i.push_back(sub_indices_s.size());
    }

  // subthreads must append to vectors in sequence

  // wait for previous subthread to finish
  for (; surfaces_step<m+2*n && !stop_worker; )
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  if (!stop_worker)
    {
      // restart counting where previous subthread ended
      i_offset = new_indices_i.size();
      s_offset = indices_s.size();
      if (indices_s.size() > 1)
	l_offset = indices_s[indices_s.size() - 2] + 1; // last is restart
      else
	l_offset = (n_bar + n_vertices) / 4;
      // copy vectors with appropriate offsets
      surfaces.insert(surfaces.end(),
		      sub_surfaces.begin(), sub_surfaces.end());
      sub_surfaces.resize(0);
      indices_s.resize(s_offset + sub_indices_s.size());
      for (std::vector<GLuint>::iterator src=sub_indices_s.begin(),
	     dest=indices_s.begin()+s_offset; src!=sub_indices_s.end();
	   ++src, ++dest)
	*dest = (*src != restart) ? (*src + l_offset) : restart;
      sub_indices_s.resize(0);
      new_indices_i.resize(i_offset + sub_indices_i.size());
      for (std::vector<GLuint>::iterator src=sub_indices_i.begin(),
	     dest=new_indices_i.begin()+i_offset; src!=sub_indices_i.end();
	   ++src, ++dest)
	*dest = *src + n_indices_[2] + s_offset;
      sub_indices_i.resize(0);
    }
  // unblock next subthread
  ++surfaces_step;
}

// draw point on the segment between two vertices
#define INTERPOLATE(i1, i2)                                          \
sub_surfaces.push_back(vertices[p[i1] - 3] +                         \
		       (vertices[p[i2] - 3] - vertices[p[i1] - 3]) * \
		       (vertices[p[i1]] - v) /                       \
		       (vertices[p[i1]] - vertices[p[i2]]));         \
sub_surfaces.push_back(vertices[p[i1] - 2] +                         \
		       (vertices[p[i2] - 2] - vertices[p[i1] - 2]) * \
		       (vertices[p[i1]] - v) /                       \
		       (vertices[p[i1]] - vertices[p[i2]]));         \
sub_surfaces.push_back(vertices[p[i1] - 1] +                         \
		       (vertices[p[i2] - 1] - vertices[p[i1] - 1]) * \
		       (vertices[p[i1]] - v) /                       \
		       (vertices[p[i1]] - vertices[p[i2]]));         \
sub_surfaces.push_back(v);                                           \
sub_indices_s.push_back(l++)

// draw surface elements
#define TRIANGLE(i1, i2, i3, i4, i5, i6) \
INTERPOLATE(i1, i2);                     \
INTERPOLATE(i3, i4);                     \
INTERPOLATE(i5, i6);                     \
sub_indices_s.push_back(restart)

#define QUAD(i1, i2, i3, i4, i5, i6, i7, i8) \
INTERPOLATE(i1, i2);                         \
INTERPOLATE(i3, i4);                         \
INTERPOLATE(i5, i6);                         \
INTERPOLATE(i7, i8);                         \
sub_indices_s.push_back(restart)

#define PENTA(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10) \
INTERPOLATE(i1, i2);                                   \
INTERPOLATE(i3, i4);                                   \
INTERPOLATE(i5, i6);                                   \
INTERPOLATE(i7, i8);                                   \
INTERPOLATE(i9, i10);                                  \
sub_indices_s.push_back(restart)

#define HEXA(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12) \
INTERPOLATE(i1, i2);                                            \
INTERPOLATE(i3, i4);                                            \
INTERPOLATE(i5, i6);                                            \
INTERPOLATE(i7, i8);                                            \
INTERPOLATE(i9, i10);                                           \
INTERPOLATE(i11, i12);                                          \
sub_indices_s.push_back(restart)

#define HEPTA(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14) \
INTERPOLATE(i1, i2);                                                       \
INTERPOLATE(i3, i4);                                                       \
INTERPOLATE(i5, i6);                                                       \
INTERPOLATE(i7, i8);                                                       \
INTERPOLATE(i9, i10);                                                      \
INTERPOLATE(i11, i12);                                                     \
INTERPOLATE(i13, i14);                                                     \
sub_indices_s.push_back(restart)

#define OCTA(i1, i2, i3, i4, i5, i6, i7, i8, i9, \
	     i10, i11, i12, i13, i14, i15, i16)  \
INTERPOLATE(i1, i2);                             \
INTERPOLATE(i3, i4);                             \
INTERPOLATE(i5, i6);                             \
INTERPOLATE(i7, i8);                             \
INTERPOLATE(i9, i10);                            \
INTERPOLATE(i11, i12);                           \
INTERPOLATE(i13, i14);                           \
INTERPOLATE(i15, i16);                           \
sub_indices_s.push_back(restart)

#define NONA(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, \
	     i11, i12, i13, i14, i15, i16, i17, i18)  \
INTERPOLATE(i1, i2);                                  \
INTERPOLATE(i3, i4);                                  \
INTERPOLATE(i5, i6);                                  \
INTERPOLATE(i7, i8);                                  \
INTERPOLATE(i9, i10);                                 \
INTERPOLATE(i11, i12);                                \
INTERPOLATE(i13, i14);                                \
INTERPOLATE(i15, i16);                                \
INTERPOLATE(i17, i18);                                \
sub_indices_s.push_back(restart)

#define DODECA(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, \
	       i14, i15, i16, i17, i18, i19, i20, i21, i22, i23, i24)  \
INTERPOLATE(i1, i2);                                                   \
INTERPOLATE(i3, i4);                                                   \
INTERPOLATE(i5, i6);                                                   \
INTERPOLATE(i7, i8);                                                   \
INTERPOLATE(i9, i10);                                                  \
INTERPOLATE(i11, i12);                                                 \
INTERPOLATE(i13, i14);                                                 \
INTERPOLATE(i15, i16);                                                 \
INTERPOLATE(i17, i18);                                                 \
INTERPOLATE(i19, i20);                                                 \
INTERPOLATE(i21, i22);                                                 \
INTERPOLATE(i23, i24);                                                 \
sub_indices_s.push_back(restart)

void gl_grid::compute_surfaces_cube
(std::vector<GLfloat> &sub_surfaces, std::vector<GLuint> &sub_indices_s,
 unsigned &l, const GLuint p[8], const int s[8],
 int min, int max, GLfloat base, GLfloat step)
{
  for (int i=min; i<max; ++i)
    {
      GLfloat v = base + (i + 1) * step; // surface value
      // check each vertex of the cube
      unsigned mask = (s[0] > i) << 0 | (s[1] > i) << 1 |
	(s[2] > i) << 2 | (s[3] > i) << 3 | (s[4] > i) << 4 |
	(s[5] > i) << 5 | (s[6] > i) << 6 | (s[7] > i) << 7;

      // connect surface points on sides of cube
      // in some cases (hepta, octa, nona, dodeca and some hexa) there
      // isn't a unique solution; I chose one that is "in between"
      switch (mask)
	{
	case 0x00: case 0xff: default: break; // this shouldn't be possible
	case 0x01: case 0xfe: TRIANGLE(0,1, 0,2, 0,4);                  break;
	case 0x02: case 0xfd: TRIANGLE(0,1, 1,5, 1,3);                  break;
	case 0x03: case 0xfc: QUAD(0,2, 0,4, 1,3, 1,5);                 break;
	case 0x04: case 0xfb: TRIANGLE(0,2, 2,3, 2,6);                  break;
	case 0x05: case 0xfa: QUAD(0,4, 0,1, 2,6, 2,3);                 break;
	case 0x06: case 0xf9: HEXA(0,1, 2,6, 0,2, 1,3, 1,5, 2,3);       break;
	case 0x07: case 0xf8: PENTA(0,4, 1,5, 2,6, 1,3, 2,3);           break;
	case 0x08: case 0xf7: TRIANGLE(2,3, 1,3, 3,7);                  break;
	case 0x09: case 0xf6: HEXA(0,2, 3,7, 2,3, 0,1, 0,4, 1,3);       break;
	case 0x0a: case 0xf5: QUAD(0,1, 1,5, 2,3, 3,7);                 break;
	case 0x0b: case 0xf4: PENTA(1,5, 3,7, 0,4, 2,3, 0,2);           break;
	case 0x0c: case 0xf3: QUAD(2,6, 0,2, 3,7, 1,3);                 break;
	case 0x0d: case 0xf2: PENTA(2,6, 0,4, 3,7, 0,1, 1,3);           break;
	case 0x0e: case 0xf1: PENTA(3,7, 2,6, 1,5, 0,2, 0,1);           break;
	case 0x0f: case 0xf0: QUAD(0,4, 1,5, 2,6, 3,7);                 break;
	case 0x10: case 0xef: TRIANGLE(0,4, 4,6, 4,5);                  break;
	case 0x11: case 0xee: QUAD(0,1, 0,2, 4,5, 4,6);                 break;
	case 0x12: case 0xed: HEXA(0,4, 1,3, 0,1, 4,5, 4,6, 1,5);       break;
	case 0x13: case 0xec: PENTA(0,2, 4,6, 1,3, 4,5, 1,5);           break;
	case 0x14: case 0xeb: HEXA(0,2, 4,5, 0,4, 2,6, 2,3, 4,6);       break;
	case 0x15: case 0xea: PENTA(0,1, 2,3, 4,5, 2,6, 4,6);           break;
	case 0x16: case 0xe9: NONA(0,4, 1,3, 1,5, 0,2, 4,6, 4,5,
				   0,1, 2,6, 2,3);                      break;
	case 0x17: case 0xe8: HEXA(1,3, 1,5, 2,3, 4,5, 2,6, 4,6);       break;
	case 0x18: case 0xe7: HEXA(0,4, 3,7, 4,5, 2,3, 4,6, 1,3);       break;
	case 0x19: case 0xe6: HEPTA(0,1, 3,7, 4,5, 2,3, 4,6, 1,3, 0,2); break;
	case 0x1a: case 0xe5: HEPTA(0,1, 4,6, 2,3, 4,5, 3,7, 0,4, 1,5); break;
	case 0x1b: case 0xe4: HEXA(0,2, 2,3, 4,6, 3,7, 4,5, 1,5);       break;
	case 0x1c: case 0xe3: HEPTA(2,6, 4,5, 3,7, 0,4, 1,3, 4,6, 0,2); break;
	case 0x1d: case 0xe2: HEXA(0,1, 1,3, 4,5, 3,7, 4,6, 2,6);       break;
	case 0x1e: case 0xe1: OCTA(2,6, 1,5, 0,4, 3,7, 4,5, 0,1,
				   4,6, 0,2);                           break;
	case 0x1f: case 0xe0: PENTA(3,7, 1,5, 2,6, 4,5, 4,6);           break;
	case 0x20: case 0xdf: TRIANGLE(1,5, 4,5, 5,7);                  break;
	case 0x21: case 0xde: HEXA(0,1, 5,7, 1,5, 0,4, 0,2, 4,5);       break;
	case 0x22: case 0xdd: QUAD(1,3, 0,1, 5,7, 4,5);                 break;
	case 0x23: case 0xdc: PENTA(1,3, 0,2, 5,7, 0,4, 4,5);           break;
	case 0x24: case 0xdb: HEXA(0,2, 5,7, 2,6, 1,5, 2,3, 4,5);       break;
	case 0x25: case 0xda: HEPTA(0,4, 5,7, 2,6, 1,5, 2,3, 4,5, 0,1); break;
	case 0x26: case 0xd9: HEPTA(1,3, 2,6, 5,7, 0,2, 4,5, 2,3, 0,1); break;
	case 0x27: case 0xd8: HEXA(0,4, 4,5, 2,6, 5,7, 2,3, 1,3);       break;
	case 0x28: case 0xd7: HEXA(1,5, 2,3, 1,3, 5,7, 4,5, 3,7);       break;
	case 0x29: case 0xd6: NONA(1,3, 0,4, 0,2, 1,5, 3,7, 2,3,
				   0,1, 5,7, 4,5);                      break;
	case 0x2a: case 0xd5: PENTA(0,1, 4,5, 2,3, 5,7, 3,7);           break;
	case 0x2b: case 0xd4: HEXA(0,4, 0,2, 4,5, 2,3, 5,7, 3,7);       break;
	case 0x2c: case 0xd3: HEPTA(1,3, 4,5, 0,2, 5,7, 2,6, 1,5, 3,7); break;
	case 0x2d: case 0xd2: OCTA(3,7, 0,4, 1,5, 2,6, 0,1, 4,5,
				   1,3, 5,7);                           break;
	case 0x2e: case 0xd1: HEXA(0,1, 0,2, 4,5, 2,6, 5,7, 3,7);       break;
	case 0x2f: case 0xd0: PENTA(2,6, 3,7, 0,4, 5,7, 4,5);           break;
	case 0x30: case 0xcf: QUAD(0,4, 4,6, 1,5, 5,7);                 break;
	case 0x31: case 0xce: PENTA(4,6, 5,7, 0,2, 1,5, 0,1);           break;
	case 0x32: case 0xcd: PENTA(5,7, 1,3, 4,6, 0,1, 0,4);           break;
	case 0x33: case 0xcc: QUAD(0,2, 4,6, 1,3, 5,7);                 break;
	case 0x34: case 0xcb: HEPTA(0,4, 2,3, 1,5, 2,6, 5,7, 0,2, 4,6); break;
	case 0x35: case 0xca: HEXA(0,1, 1,5, 2,3, 5,7, 2,6, 4,6);       break;
	case 0x36: case 0xc9: OCTA(1,3, 4,6, 0,2, 5,7, 2,6, 0,4,
				   2,3, 0,1);                           break;
	case 0x37: case 0xc8: PENTA(5,7, 4,6, 1,3, 2,6, 2,3);           break;
	case 0x38: case 0xc7: HEPTA(5,7, 2,3, 4,6, 1,3, 0,4, 3,7, 1,5); break;
	case 0x39: case 0xc6: OCTA(5,7, 0,2, 1,3, 4,6, 2,3, 0,1,
				   3,7, 1,5);                           break;
	case 0x3a: case 0xc5: HEXA(0,1, 0,4, 2,3, 4,6, 3,7, 5,7);       break;
	case 0x3b: case 0xc4: PENTA(4,6, 0,2, 5,7, 2,3, 3,7);           break;
	case 0x3c: case 0xc3: OCTA(0,2, 5,7, 1,3, 4,6, 1,5, 2,6,
				   0,4, 3,7);                           break;
	case 0x3d: case 0xc2: HEPTA(3,7, 0,1, 2,6, 1,5, 4,6, 1,3, 5,7); break;
	case 0x3e: case 0xc1: HEPTA(4,6, 0,1, 5,7, 0,2, 3,7, 0,4, 2,6); break;
	case 0x3f: case 0xc0: QUAD(4,6, 2,6, 5,7, 3,7);                 break;
	case 0x40: case 0xbf: TRIANGLE(4,6, 2,6, 6,7);                  break;
	case 0x41: case 0xbe: HEXA(0,4, 6,7, 4,6, 0,2, 0,1, 2,6);       break;
	case 0x42: case 0xbd: HEXA(0,1, 6,7, 1,3, 4,6, 1,5, 2,6);       break;
	case 0x43: case 0xbc: HEPTA(0,2, 6,7, 1,3, 4,6, 1,5, 2,6, 0,4); break;
	case 0x44: case 0xbb: QUAD(0,2, 2,3, 4,6, 6,7);                 break;
	case 0x45: case 0xba: PENTA(2,3, 6,7, 0,1, 4,6, 0,4);           break;
	case 0x46: case 0xb9: HEPTA(0,2, 1,5, 4,6, 1,3, 6,7, 0,1, 2,3); break;
	case 0x47: case 0xb8: HEXA(0,4, 4,6, 1,5, 6,7, 1,3, 2,3);       break;
	case 0x48: case 0xb7: HEXA(2,3, 4,6, 2,6, 3,7, 1,3, 6,7);       break;
	case 0x49: case 0xb6: NONA(2,6, 0,1, 0,4, 2,3, 6,7, 4,6,
				   0,2, 3,7, 1,3);                      break;
	case 0x4a: case 0xb5: HEPTA(3,7, 4,6, 1,5, 2,6, 0,1, 6,7, 2,3); break;
	case 0x4b: case 0xb4: OCTA(3,7, 0,4, 2,6, 1,5, 4,6, 0,2,
				   6,7, 2,3);                           break;
	case 0x4c: case 0xb3: PENTA(0,2, 1,3, 4,6, 3,7, 6,7);           break;
	case 0x4d: case 0xb2: HEXA(0,1, 0,4, 1,3, 4,6, 3,7, 6,7);       break;
	case 0x4e: case 0xb1: HEXA(0,2, 0,1, 4,6, 1,5, 6,7, 3,7);       break;
	case 0x4f: case 0xb0: PENTA(1,5, 0,4, 3,7, 4,6, 6,7);           break;
	case 0x50: case 0xaf: QUAD(4,5, 0,4, 6,7, 2,6);                 break;
	case 0x51: case 0xae: PENTA(4,5, 0,1, 6,7, 0,2, 2,6);           break;
	case 0x52: case 0xad: HEPTA(4,5, 1,3, 6,7, 0,1, 2,6, 1,5, 0,4); break;
	case 0x53: case 0xac: HEXA(0,2, 2,6, 1,3, 6,7, 1,5, 4,5);       break;
	case 0x54: case 0xab: PENTA(6,7, 4,5, 2,3, 0,4, 0,2);           break;
	case 0x55: case 0xaa: QUAD(0,1, 2,3, 4,5, 6,7);                 break;
	case 0x56: case 0xa9: OCTA(4,5, 2,3, 0,1, 6,7, 1,3, 0,2,
				   1,5, 0,4);                           break;
	case 0x57: case 0xa8: PENTA(6,7, 2,3, 4,5, 1,3, 1,5);           break;
	case 0x58: case 0xa7: HEPTA(2,6, 1,3, 0,4, 3,7, 4,5, 2,3, 6,7); break;
	case 0x59: case 0xa6: OCTA(6,7, 0,1, 2,3, 4,5, 0,2, 1,3,
				   2,6, 3,7);                           break;
	case 0x5a: case 0xa5: OCTA(0,4, 3,7, 2,6, 1,5, 2,3, 4,5,
				   0,1, 6,7);                           break;
	case 0x5b: case 0xa4: HEPTA(6,7, 0,2, 4,5, 2,3, 1,5, 2,6, 3,7); break;
	case 0x5c: case 0xa3: HEXA(0,2, 0,4, 1,3, 4,5, 3,7, 6,7);       break;
	case 0x5d: case 0xa2: PENTA(4,5, 6,7, 0,1, 3,7, 1,3);           break;
	case 0x5e: case 0xa1: HEPTA(1,5, 0,2, 3,7, 0,4, 6,7, 0,1, 4,5); break;
	case 0x5f: case 0xa0: QUAD(1,5, 4,5, 3,7, 6,7);                 break;
	case 0x60: case 0x9f: HEXA(4,6, 1,5, 4,5, 6,7, 2,6, 5,7);       break;
	case 0x61: case 0x9e: NONA(4,5, 0,2, 0,1, 4,6, 5,7, 1,5,
				   0,4, 6,7, 2,6);                      break;
	case 0x62: case 0x9d: HEPTA(4,5, 2,6, 0,1, 6,7, 1,3, 4,6, 5,7); break;
	case 0x63: case 0x9c: OCTA(5,7, 0,2, 4,6, 1,3, 0,4, 2,6,
				   4,5, 6,7);                           break;
	case 0x64: case 0x9b: HEPTA(6,7, 1,5, 2,3, 4,5, 0,2, 5,7, 4,6); break;
	case 0x65: case 0x9a: OCTA(6,7, 0,1, 4,5, 2,3, 1,5, 0,4,
				   5,7, 4,6);                           break;
	case 0x66: case 0x99: OCTA(0,1, 6,7, 4,5, 2,3, 4,6, 1,3,
				   0,2, 5,7);                           break;
	case 0x67: case 0x98: HEPTA(5,7, 0,4, 1,3, 4,6, 2,3, 4,5, 6,7); break;
	case 0x68: case 0x97: NONA(6,7, 1,3, 2,3, 5,7, 4,6, 2,6,
				   3,7, 4,5, 1,5);                      break;
	case 0x69: case 0x96: DODECA(0,1, 6,7, 4,5, 2,3, 4,6, 1,3,
				     5,7, 0,2, 1,5, 2,6, 3,7, 0,4);     break;
	case 0x6a: case 0x95: OCTA(2,3, 4,5, 6,7, 0,1, 5,7, 4,6,
				   3,7, 2,6);                           break;
	case 0x6b: case 0x94: NONA(6,7, 0,4, 4,5, 2,6, 3,7, 5,7,
				   4,6, 2,3, 0,2);                      break;
	case 0x6c: case 0x93: OCTA(4,6, 1,3, 5,7, 0,2, 3,7, 1,5,
				   6,7, 4,5);                           break;
	case 0x6d: case 0x92: NONA(5,7, 0,1, 1,3, 4,5, 6,7, 3,7,
				   1,5, 4,6, 0,4);                      break;
	case 0x6e: case 0x91: HEPTA(4,6, 3,7, 0,2, 5,7, 0,1, 6,7, 4,5); break;
	case 0x6f: case 0x90: HEXA(4,5, 3,7, 5,7, 4,6, 0,4, 6,7);       break;
	case 0x70: case 0x8f: PENTA(0,4, 2,6, 1,5, 6,7, 5,7);           break;
	case 0x71: case 0x8e: HEXA(0,2, 0,1, 2,6, 1,5, 6,7, 5,7);       break;
	case 0x72: case 0x8d: HEXA(0,4, 0,1, 2,6, 1,3, 6,7, 5,7);       break;
	case 0x73: case 0x8c: PENTA(1,3, 5,7, 0,2, 6,7, 2,6);           break;
	case 0x74: case 0x8b: HEXA(0,4, 0,2, 1,5, 2,3, 5,7, 6,7);       break;
	case 0x75: case 0x8a: PENTA(2,3, 0,1, 6,7, 1,5, 5,7);           break;
	case 0x76: case 0x89: HEPTA(2,3, 0,4, 6,7, 0,1, 5,7, 0,2, 1,3); break;
	case 0x77: case 0x88: QUAD(2,3, 1,3, 6,7, 5,7);                 break;
	case 0x78: case 0x87: OCTA(1,5, 2,6, 3,7, 0,4, 6,7, 2,3,
				   5,7, 1,3);                           break;
	case 0x79: case 0x86: NONA(3,7, 0,2, 2,6, 1,3, 5,7, 6,7,
				   2,3, 1,5, 0,1);                      break;
	case 0x7a: case 0x85: HEPTA(2,3, 5,7, 0,1, 6,7, 0,4, 3,7, 2,6); break;
	case 0x7b: case 0x84: HEXA(2,6, 5,7, 6,7, 2,3, 0,2, 3,7);       break;
	case 0x7c: case 0x83: HEPTA(1,5, 6,7, 0,4, 3,7, 0,2, 5,7, 1,3); break;
	case 0x7d: case 0x82: HEXA(1,3, 6,7, 3,7, 1,5, 0,1, 5,7);       break;
	case 0x7e: case 0x81: HEXA(5,7, 0,2, 3,7, 0,4, 6,7, 0,1);       break;
	case 0x7f: case 0x80: TRIANGLE(3,7, 5,7, 6,7);                  break;
	}
    }
}

#undef INTERPOLATE
#undef TRIANGLE
#undef QUAD
#undef PENTA
#undef HEXA
#undef HEPTA
#undef OCTA
#undef NONA
#undef DODECA

void gl_grid::copy_values()
{
  refresh_vbuffer = false;

  glBindVertexArray(h_vertex_array);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer);
  glBufferData(GL_ARRAY_BUFFER,
	       (n_vertices + surfaces.size() + n_bar) * sizeof(GLfloat),
	       NULL, GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 0, n_bar * sizeof(GLfloat),
		  &vertices_bar[0]);
  glBufferSubData(GL_ARRAY_BUFFER, n_bar * sizeof(GLfloat),
		  n_vertices * sizeof(GLfloat), &vertices[0]);
  glBufferSubData(GL_ARRAY_BUFFER, (n_bar + n_vertices) * sizeof(GLfloat),
		  surfaces.size() * sizeof(GLfloat), &surfaces[0]);

  vmax = new_vmax;
  vscale = new_vscale;
  size[0] = new_size[0];
  size[1] = new_size[1];
  size[2] = new_size[2];
  scale1 = new_scale;
  scale2 = 1.0;
  fix_new_scale = false;

  // signal completion
  values_signal.emit(vmax);
  scale_signal.emit(scale1);
}

void gl_grid::copy_indices()
{
  refresh_ibuffer = false;

  glBindVertexArray(h_vertex_array);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER,
	       (new_n_indices[2] + indices_s.size()) * sizeof(GLuint),
	       NULL, GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0,
		  new_n_indices[2] * sizeof(GLuint), &indices[0]);
  glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, new_n_indices[2] * sizeof(GLuint),
		  indices_s.size() * sizeof(GLuint), &indices_s[0]);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

  step1[0] = new_step1[0];
  step1[1] = new_step1[1];
  step1[2] = new_step1[2];
  step2[0] = new_step2[0];
  step2[1] = new_step2[1];
  step2[2] = new_step2[2];
  n_surfaces = new_n_surfaces;
  n_planes[0] = new_n_planes[0];
  n_planes[1] = new_n_planes[1];
  n_planes[2] = new_n_planes[2];
  n_slices[0] = 0;
  n_slices[1] = n_slices[0] + new_n_planes[0] - 1;
  n_slices[2] = n_slices[1] + new_n_planes[1] - 1;
  n_stride[0] = new_n_stride[0];
  n_stride[1] = new_n_stride[1];
  n_stride[2] = new_n_stride[2];
  n_indices[0] = new_n_indices[0];
  n_indices[1] = new_n_indices[1];
  n_indices[2] = new_n_indices[2];
  indices_i = new_indices_i;
}

void gl_grid::worker_grid()
{
  // this method runs in a separate thread

  if (compute_grid())
    refresh_values = refresh_indices = refresh_surfaces = true;

  working_grid = false; // done
}

void gl_grid::worker_values()
{
  // this method runs in a separate thread

  if (compute_values())
    refresh_surfaces = true;

  working_values = false; // done
}

void gl_grid::worker_indices()
{
  // this method runs in a separate thread

  if (compute_indices())
    refresh_surfaces = true;

  working_indices = false; // done
}

void gl_grid::worker_surfaces()
{
  // this method runs in a separate thread

  if (compute_surfaces())
    refresh_vbuffer = refresh_ibuffer = true;

  working_surfaces = false; // done
}

void gl_grid::terminate_worker()
{
  if (worker)
    {
      stop_worker = true; // end now
      worker->join();
      delete worker;
      worker = 0;
    }
  stop_worker = false;
}

void gl_grid::refresh()
{
  // drive updating OpenGL buffers as needed

  // heavy computations are run in a worker thread for efficiency
  // however it seems that OpenGL commands must be run in the main thread
  // so data vectors are computed and copied to buffers in separate methods
  // computing can be done in the worker thread, copying cannot

  // run only one worker thread at a time
  // if the current worker becomes useless because the data it is computing
  // needs refreshing, stop it and restart

  // all buffers that need refreshing must be copied at the same time
  // so that their contents are consistent whenever draw() is run

  // when grid is recomputed, values and indices must be recomputed too
  // so refresh_grid takes over everything else

  // when either values or indices are recomputed, surfaces must be
  // recomputed too
  // so refresh_values, refresh_indices take over refresh_surfaces
  if (refresh_grid)
    {
      // recompute grid: sets refresh_values, refresh_indices, refresh_surfaces
      terminate_worker();
      working_grid = true;
      worker = new std::thread(&gl_grid::worker_grid, this);
      working_signal.emit(true);
    }
  // refresh values if needed, unless another computation is already running
  // takes over refresh_surfaces
  else if (refresh_values && !working_grid && !working_indices)
    {
      // recompute values: sets refresh_surfaces
      terminate_worker();
      working_values = true;
      worker = new std::thread(&gl_grid::worker_values, this);
      working_signal.emit(true);
    }
  // refresh indices if needed, unless another computation is already running
  // takes over refresh_surfaces
  else if (refresh_indices && !working_grid && !working_values)
    {
      // recompute indices: sets refresh_surfaces
      terminate_worker();
      working_indices = true;
      worker = new std::thread(&gl_grid::worker_indices, this);
      working_signal.emit(true);
    }
  // refresh surfaces if needed, unless another computation is already running
  else if (refresh_surfaces && !working_grid && !working_values &&
	   !working_indices)
    {
      // recompute surfaces
      terminate_worker();
      working_surfaces = true;
      worker = new std::thread(&gl_grid::worker_surfaces, this);
      working_signal.emit(true);
    }
  // if worker thread is done, delete it
  else if (worker && !working_grid && !working_values && !working_indices &&
	   !working_surfaces)
    {
      terminate_worker();
    }
  // when all computations are done, update buffers
  else if (refresh_vbuffer && !refresh_indices &&
	   !working_indices && !working_surfaces)
    {
      // update values buffer
      copy_values();
      // if both buffers needs updating, ensure they are done at the same time
      if (refresh_ibuffer)
	copy_indices();
      working_signal.emit(false);
    }
  else if (refresh_ibuffer && !refresh_values &&
	   !working_values && !working_surfaces)
    {
      // update indices buffer
      copy_indices();
      // see above -- but then it would run the above branch and not get here?
      // this shouldn't harm anyway
      if (refresh_vbuffer)
	copy_values();
      working_signal.emit(false);
    }
}

gl_grid::gl_grid()
  : f(0),
    scale1(1.0), scale2(1.0), scale_margin(0.0),
    vmax(1.0), vscale(1.0), ascale(1.0),
    new_n_surfaces(20),
    new_scale(1.0), new_vmax(1.0), new_vscale(1.0),
    fix_vmax(false), fix_new_scale(false),
    refresh_grid(true), refresh_values(false), refresh_indices(false),
    refresh_vbuffer(false), refresh_ibuffer(false),
    h_vertex_array(0), h_vertex_buffer(0), h_index_buffer(0),
    worker(0), working_grid(false), working_values(false),
    working_indices(false), working_surfaces(false), stop_worker(false),
    nthreads(1),
    draw_surfaces(true), draw_volume(true), draw_bar(true), clip(true)
{
  size[0] = size[1] = size[2] = 0;
}

gl_grid::~gl_grid()
{
  terminate_worker();
}

void gl_grid::init()
{
  // build vertex shader
  const GLchar *vertex_source =
    "#version 330                                                     \n"
    "                                                                   "
    "uniform mat4 u_matrix;                                             "
    "uniform float u_value, u_alpha;                                    "
    "uniform bool u_zero;                                               "
    "uniform vec3 u_scale;                                              "
    "                                                                   "
    "in vec3 in_position;                                               "
    "in float in_value;                                                 "
    "                                                                   "
    "out vec4 color;                                                    "
    "                                                                   "
    "/*                                                                 "
    "  interpolate between:                                             "
    "  c = -clamp  color = blue,         alpha = 1                      "
    "  c = -step   color = cyan,         alpha = 1                      "
    "  c = 0       color = cyan/green,   alpha = 0                      "
    "  there is a discontinuity at 0 to differentiate clearly           "
    "  between positive and negative                                    "
    "  c = 0       color = yellow/green, alpha = 0                      "
    "  c = step    color = yellow,       alpha = 1                      "
    "  c = clamp   color = red,          alpha = 1                      "
    "*/                                                                 "
    "vec4 color_scale(float x)                                          "
    "{                                                                  "
    "  const float clamp = 1.0, step = 0.25;                            "
    "  if (x < -clamp)                                                  "
    "    x = -clamp;                                                    "
    "  else if (x > clamp)                                              "
    "    x = clamp;                                                     "
    "  if (x < -step)                                                   "
    "    return vec4(0.0, (clamp+x)/(clamp-step), 1.0, 1.0);            "
    "  else if (x < 0.0)                                                "
    "    {                                                              "
    "      if (u_zero)                                                  "
    "        return vec4(0.0, 1.0, 1.0-(step+x)/(clamp-step), -x/step); "
    "      else                                                         "
    "        return vec4(0.0, 1.0, 1.0-(step+x)/(clamp-step), 1.0);     "
    "    }                                                              "
    "  else if (x < step)                                               "
    "    {                                                              "
    "      if (u_zero)                                                  "
    "        return vec4(1.0-(step-x)/(clamp-step), 1.0, 0.0, x/step);  "
    "      else                                                         "
    "        return vec4(1.0-(step-x)/(clamp-step), 1.0, 0.0, 1.0);     "
    "    }                                                              "
    "  else                                                             "
    "    return vec4(1.0, (clamp-x)/(clamp-step), 0.0, 1.0);            "
    "}                                                                  "
    "                                                                   "
    "void main()                                                        "
    "{                                                                  "
    "  gl_Position = u_matrix * vec4(in_position, 1.0);                 "
    "  color = color_scale(in_value * u_value) *                        "
    "    vec4(1.0, 1.0, 1.0, u_alpha);                                  "
    "                                                                   "
    "  /* set clipping planes */                                        "
    "  gl_ClipDistance[0] = in_position[0] + u_scale[0];                "
    "  gl_ClipDistance[1] = in_position[1] + u_scale[1];                "
    "  gl_ClipDistance[2] = in_position[2] + u_scale[2];                "
    "  gl_ClipDistance[3] = u_scale[0] - in_position[0];                "
    "  gl_ClipDistance[4] = u_scale[1] - in_position[1];                "
    "  gl_ClipDistance[5] = u_scale[2] - in_position[2];                "
    "}                                                                  ";
  h_vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(h_vertex_shader, 1, &vertex_source, NULL);
  glCompileShader(h_vertex_shader);

  // build fragment shader
  const GLchar *fragment_source =
    "#version 330       \n"
    "                     "
    "in vec4 color;       "
    "                     "
    "out vec4 out_color;  "
    "                     "
    "void main()          "
    "{                    "
    "  out_color = color; "
    "}                    ";
  h_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(h_fragment_shader, 1, &fragment_source, NULL);
  glCompileShader(h_fragment_shader);

  // build shader program
  h_program = glCreateProgram();
  glAttachShader(h_program, h_fragment_shader);
  glAttachShader(h_program, h_vertex_shader);
  glLinkProgram(h_program);

  // uniform variables
  u_matrix = glGetUniformLocation(h_program, "u_matrix");
  u_value = glGetUniformLocation(h_program, "u_value");
  u_alpha = glGetUniformLocation(h_program, "u_alpha");
  u_zero = glGetUniformLocation(h_program, "u_zero");
  u_scale = glGetUniformLocation(h_program, "u_scale");

  glGenVertexArrays(1, &h_vertex_array);
  glBindVertexArray(h_vertex_array);
  glGenBuffers(1, &h_vertex_buffer);
  glGenBuffers(1, &h_index_buffer);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer);
  glEnableVertexAttribArray(0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4,
			(void *) 0);
  glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4,
			(void *) (3 * sizeof(GLfloat)));

  glEnable(GL_CLIP_PLANE0);
  glEnable(GL_CLIP_PLANE1);
  glEnable(GL_CLIP_PLANE2);
  glEnable(GL_CLIP_PLANE3);
  glEnable(GL_CLIP_PLANE4);
  glEnable(GL_CLIP_PLANE5);

  // scale bar
  const GLfloat clamp = 1.0, step = 0.25;
  GLfloat v[] =
    {
      -1.0, -clamp, 0.0, -clamp,
       1.0, -clamp, 0.0, -clamp,
      -1.0, -step,  0.0,  -step,
       1.0, -step,  0.0,  -step,
      -1.0,   0.0,  0.0,    0.0,
       1.0,   0.0,  0.0,    0.0,
      -1.0,  step,  0.0,   step,
       1.0,  step,  0.0,   step,
      -1.0,  clamp, 0.0,  clamp,
       1.0,  clamp, 0.0,  clamp,
    };
  n_bar = sizeof(v) / sizeof(v[0]);
  vertices_bar.resize(n_bar);
  for (int i=0; i<n_bar; ++i)
    vertices_bar[i] = v[i];
}

void gl_grid::init_size(const GLuint size_[3])
{
  // this method runs in the main thread

  terminate_worker();
  working_signal.emit(true);

  new_size[0] = size_[0];
  new_size[1] = size_[1];
  new_size[2] = size_[2];
  compute_grid();
  compute_values();

  working_signal.emit(false);
}

void gl_grid::init_step(const GLuint step1_[3], const GLuint step2_[3])
{
  // this method runs in the main thread

  terminate_worker();
  working_signal.emit(true);

  new_step1[0] = step1_[0];
  new_step1[1] = step1_[1];
  new_step1[2] = step1_[2];
  new_step2[0] = step2_[0];
  new_step2[1] = step2_[1];
  new_step2[2] = step2_[2];
  compute_indices();
  compute_surfaces();
  copy_values();
  copy_indices();

  working_signal.emit(false);
}

void gl_grid::set_grid(const GLuint size_[3], const GLuint step1_[3],
		       const GLuint step2_[3])
{
  if (size_[0] != new_size[0] || size_[1] != new_size[1] ||
      size_[2] != new_size[2])
    refresh_grid = true;
  else if (step1_[0] != new_step1[0] || step1_[1] != new_step1[1] ||
	   step1_[2] != new_step1[2] || step2_[0] != new_step2[0] ||
	   step2_[1] != new_step2[1] || step2_[2] != new_step2[2])
    refresh_indices = true;

  new_size[0] = size_[0];
  new_size[1] = size_[1];
  new_size[2] = size_[2];
  new_step1[0] = step1_[0];
  new_step1[1] = step1_[1];
  new_step1[2] = step1_[2];
  new_step2[0] = step2_[0];
  new_step2[1] = step2_[1];
  new_step2[2] = step2_[2];
}

void gl_grid::set_nsurfaces(unsigned nsurfaces_)
{
  new_n_surfaces = nsurfaces_;
  refresh_surfaces = true;
}

void gl_grid::set_scale(double scale_)
{
  if (!fix_new_scale)
    new_scale = scale_; // this has no effect until compute_values() is run

  if (f && (scale_ > scale1 * (1.0 + scale_margin) ||
	    scale_ * (1.0 + scale_margin) < scale1))
    // when the cumulative change in scale goes beyond scale_margin
    // scale the grid and recompute values
    refresh_values = true;

  // zoom the grid
  scale2 = scale_ / scale1;
  scale_signal.emit(scale1 * scale2);
}

void gl_grid::set_new_scale(double scale_)
{
  // deferred scale change
  new_scale = scale_;
  fix_new_scale = true;
}

void gl_grid::set_vscale(double vscale_)
{
  vscale = vscale_;
}

void gl_grid::set_values(const GLfloat *values_)
{
  for (unsigned i=0, l=3; l<n_vertices; ++i, l+=4)
    vertices[l] = values_[i];

  refresh_values = true;
}

bool gl_grid::read_values(std::istream &is)
{
  // fixed values, no function
  f = 0;

  if (!fix_vmax)
    new_vmax = 0.0;
  // read values
  // new_size[*] == 0 means dimensions aren't set
  // in that case they are deduced from the file format
  std::vector <GLfloat> values_, values_line;
  unsigned n_lines = 0;
  while (is.good() &&
	 (new_size[0] == 0 || new_size[1] == 0 || new_size[2] == 0 ||
	  values_.size() <
	  (new_size[0] + 1) * (new_size[1] + 1) * (new_size[2] + 1)))
    {
      // read file line by line
      std::string str;
      std::getline(is, str);
      // read values in current line
      std::istringstream ss(str);
      values_line.resize(0);
      GLfloat v;
      while (ss >> v)
	{
	  if (!fix_vmax)
	    {
	      if (v > new_vmax)
		new_vmax = v;
	      else if (v < -new_vmax)
		new_vmax = -v;
	    }
	  values_line.push_back(v);
	}
      if (values_line.size() == 0)
	{
	  if (new_size[1] == 0 && n_lines > 0)
	    // empty line marks end of plane, set second dimension
	    new_size[1] = n_lines - 1;
	  // else skip line
	}
      else
	{
	  ++n_lines; // data line
	  if (new_size[0] == 0)
	    // set first dimension
	    new_size[0] = values_line.size() - 1;
	  // append line
	  values_.insert(values_.end(),
			 values_line.begin(), values_line.end());
	}
    }
  if (new_size[0] == 0 || new_size[1] == 0)
    return false; // dimensions not set
  if (new_size[2] == 0)
    {
      // set third dimension
      if (n_lines / (new_size[1] + 1) < 2)
	return false; // not enough planes
      new_size[2] = n_lines / (new_size[1] + 1) - 1;
    }
  else if (n_lines / (new_size[1] + 1) < new_size[2] + 1)
    return false; // not enough planes
  if (!fix_vmax && new_vmax == 0.0)
    new_vmax = 1.0;
  vmax = new_vmax;

  compute_grid();
  set_values(values_);

  return true;
}

bool gl_grid::read_values(const Glib::ustring &file)
{
  if (file == "" || file == "-")
    return read_values(std::cin);
  else
    {
      std::ifstream is(file);
      return read_values(is);
    }
}

void gl_grid::set_view_matrix(const GLdouble *matrix_)
{
  for (GLuint i=0; i<16; ++i)
    view_matrix[i] = matrix_[i];
}

void gl_grid::set_model_matrix(const GLdouble *matrix_)
{
  for (GLuint i=0; i<16; ++i)
    model_matrix[i] = matrix_[i];

  // choose which set of planes to draw depending on orientation

  // choose the set of planes that are most parallel to the screen
  // and draw them from the farthest to the nearest

  // components of unit vectors along axes
  double x[] = {
    model_matrix[0], model_matrix[4], model_matrix[8],
  }, y[] = {
    model_matrix[1], model_matrix[5], model_matrix[9],
  }, z[] = {
    model_matrix[2], model_matrix[6], model_matrix[10],
  };
  // choose the largest negative value of y
  // that is, the unit vector that comes out from the screen the most
  // (y axis points into the screen)
  double min = 1e+100;
  for (unsigned i=0; i<sizeof(y)/sizeof(y[0]); ++i)
    {
      if (y[i] < min)
	{
	  min = y[i];
	  axis = i;
	  reverse = false;
	  cos = std::sqrt(y[i]*y[i] / (x[i]*x[i] + y[i]*y[i] + z[i]*z[i]));
	}
      if (-y[i] < min)
	{
	  min = -y[i];
	  axis = i;
	  reverse = true;
	  cos = std::sqrt(y[i]*y[i] / (x[i]*x[i] + y[i]*y[i] + z[i]*z[i]));
	}
    }
}

void gl_grid::draw()
{
  refresh(); // recompute and update buffers as needed

  if (size[0] == 0 || n_indices[2] == 0)
    return;

  GLdouble zoom[] = { 1.0 / scale2, 1.0 / scale2, 1.0 / scale2, 1.0, };
  GLfloat matrix[16];
  // multiply matrices, apply scale
  for (unsigned i=0; i<4; ++i)
    for (unsigned j=0; j<4; ++j)
      {
	matrix[i*4+j] = 0.0;
	for (unsigned k=0; k<4; ++k)
	  matrix[i*4+j] += zoom[i] * view_matrix[j+k*4] * model_matrix[i*4+k];
      }

  // set transparency level
  double ascale1 = ascale * (draw_volume ? 1.0 : 6.0) / n_surfaces;
  double ascale2 = ascale * (draw_surfaces ? 6.0 : 7.0) *
    (1.0 + scale_margin) / scale2 / n_planes[axis] / cos;

  // clipping scale
  GLfloat huge_f = 1e+30;
  GLfloat cscale = clip ? (GLfloat) scale2 : huge_f;
  GLfloat cscalev[] = {
    (GLfloat) cscale, (GLfloat) cscale, (GLfloat) cscale,
  };

  // OpenGL commands

  glUseProgram(h_program);
  glBindVertexArray(h_vertex_array);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer);

  const GLuint restart = static_cast<GLuint>(-1);
  glEnable(GL_PRIMITIVE_RESTART);
  glPrimitiveRestartIndex(restart);

  glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix);
  glUniform1f(u_value, vscale);
  glUniform3fv(u_scale, 1, cscalev);

  if (!reverse)
    // draw planes in straight order
    for (GLuint n=0; n<n_planes[axis]; ++n)
      {
	// isosurfaces
	if (draw_surfaces && n > 0)
	  {
	    glUniform1f(u_alpha, ascale1);
	    glUniform1i(u_zero, false);
	    glDrawElements
	      (GL_TRIANGLE_STRIP,
	       indices_i[n_slices[axis] + n] -
	       indices_i[n_slices[axis] + n - 1],
	       GL_UNSIGNED_INT,
	       (void *) (indices_i[n_slices[axis] + n - 1] * sizeof(GLuint)));
	  }

	// volume rendering
	if (draw_volume)
	  {
	    glUniform1f(u_alpha, ascale2);
	    glUniform1i(u_zero, true);
	    glDrawElements
	      (GL_TRIANGLE_STRIP, n_stride[axis], GL_UNSIGNED_INT,
	       (void *) ((((axis == 0) ? 0 : n_indices[axis - 1]) +
			  n_stride[axis] * n) * sizeof(GLuint)));
	  }
      }
  else
    // draw planes in reverse order
    for (GLuint n=0; n<n_planes[axis]; ++n)
      {
	// isosurfaces
	if (draw_surfaces && n > 0)
	  {
	    glUniform1f(u_alpha, ascale1);
	    glUniform1i(u_zero, false);
	    glDrawElements
	      (GL_TRIANGLE_STRIP,
	       indices_i[n_slices[axis] + n_planes[axis] - n] -
	       indices_i[n_slices[axis] + n_planes[axis] - n - 1],
	       GL_UNSIGNED_INT,
	       (void *) (indices_i[n_slices[axis] + n_planes[axis] - n - 1] *
			 sizeof(GLuint)));
	  }

	// volume rendering
	if (draw_volume)
	  {
	    glUniform1f(u_alpha, ascale2);
	    glUniform1i(u_zero, true);
	    glDrawElements
	      (GL_TRIANGLE_STRIP, n_stride[axis], GL_UNSIGNED_INT,
	       (void *) ((((axis == 0) ? 0 : n_indices[axis - 1]) +
			  n_stride[axis] * (n_planes[axis] - n - 1)) *
			 sizeof(GLuint)));
	  }
      }

  if (draw_bar)
    {
      GLfloat bar_width = (GLfloat) bar_size / width,
	bar_pos = (GLfloat) 1.0 - 3.0 * bar_size / width;
      GLfloat matrix_bar[] = {
	bar_width, 0.0, 0.0, 0.0,
	0.0,       0.8, 0.0, 0.0,
	0.0,       0.0, 1.0, 0.0,
	bar_pos,   0.0, 0.0, 1.0,
      };
      GLfloat cscalev[] = { huge_f, huge_f, huge_f, }; // don't clip

      glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix_bar);
      glUniform1f(u_value, 1.0);
      glUniform1f(u_alpha, 1.0);
      glUniform1i(u_zero, true);
      glUniform3fv(u_scale, 1, cscalev);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, n_bar / 4);
    }

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
  glUseProgram(0);
}
