hist/RH3Painter.mjs

import { gStyle, settings, kNoZoom, kInspect } from '../core.mjs';
import { Matrix4, Mesh, MeshBasicMaterial, MeshLambertMaterial, SphereGeometry,
         LineBasicMaterial, BufferAttribute, BufferGeometry } from '../three.mjs';
import { floatToString, TRandom } from '../base/BasePainter.mjs';
import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs';
import { RAxisPainter } from '../gpad/RAxisPainter.mjs';
import { RHistPainter } from '../hist2d/RHistPainter.mjs';
import { createLineSegments, PointsCreator, Box3D } from '../base/base3d.mjs';
import { RH1Painter } from './RH1Painter.mjs';
import { RH2Painter } from './RH2Painter.mjs';
import { assignFrame3DMethods } from './hist3d.mjs';

/**
 * @summary Painter for RH3 classes
 *
 * @private
 */

class RH3Painter extends RHistPainter {

   /** @summary Returns histogram dimension */
   getDimension() { return 3; }

   scanContent(when_axis_changed) {
      // no need to rescan histogram while result does not depend from axis selection
      if (when_axis_changed && this.nbinsx && this.nbinsy && this.nbinsz) return;

      const histo = this.getHisto();
      if (!histo) return;

      this.extractAxesProperties(3);

      // global min/max, used at the moment in 3D drawing

      if (this.isDisplayItem()) {
         // take min/max values from the display item
         this.gminbin = histo.fContMin;
         this.gminposbin = histo.fContMinPos > 0 ? histo.fContMinPos : null;
         this.gmaxbin = histo.fContMax;
      } else {
         this.gminbin = this.gmaxbin = histo.getBinContent(1, 1, 1);

         for (let i = 0; i < this.nbinsx; ++i) {
            for (let j = 0; j < this.nbinsy; ++j) {
               for (let k = 0; k < this.nbinsz; ++k) {
                  const bin_content = histo.getBinContent(i+1, j+1, k+1);
                  if (bin_content < this.gminbin) this.gminbin = bin_content; else
                  if (bin_content > this.gmaxbin) this.gmaxbin = bin_content;
               }
            }
         }
      }

      this.draw_content = (this.gmaxbin !== 0) || (this.gminbin !== 0);
   }

  /** @summary Count histogram statistic */
   countStat() {
      const histo = this.getHisto(),
            xaxis = this.getAxis('x'),
            yaxis = this.getAxis('y'),
            zaxis = this.getAxis('z'),
            i1 = this.getSelectIndex('x', 'left'),
            i2 = this.getSelectIndex('x', 'right'),
            j1 = this.getSelectIndex('y', 'left'),
            j2 = this.getSelectIndex('y', 'right'),
            k1 = this.getSelectIndex('z', 'left'),
            k2 = this.getSelectIndex('z', 'right'),
            res = { name: histo.fName, entries: 0, integral: 0, meanx: 0, meany: 0, meanz: 0, rmsx: 0, rmsy: 0, rmsz: 0 };
      let stat_sum0 = 0, stat_sumx1 = 0, stat_sumy1 = 0,
          stat_sumz1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, stat_sumz2 = 0,
          xi, yi, zi, xx, xside, yy, yside, zz, zside, cont;

      for (xi = 1; xi <= this.nbinsx; ++xi) {
         xx = xaxis.GetBinCoord(xi - 0.5);
         xside = (xi <= i1+1) ? 0 : (xi > i2+1 ? 2 : 1);

         for (yi = 1; yi <= this.nbinsy; ++yi) {
            yy = yaxis.GetBinCoord(yi - 0.5);
            yside = (yi <= j1+1) ? 0 : (yi > j2+1 ? 2 : 1);

            for (zi = 1; zi <= this.nbinsz; ++zi) {
               zz = zaxis.GetBinCoord(zi - 0.5);
               zside = (zi <= k1+1) ? 0 : (zi > k2+1 ? 2 : 1);

               cont = histo.getBinContent(xi, yi, zi);
               res.entries += cont;

               if ((xside === 1) && (yside === 1) && (zside === 1)) {
                  stat_sum0 += cont;
                  stat_sumx1 += xx * cont;
                  stat_sumy1 += yy * cont;
                  stat_sumz1 += zz * cont;
                  stat_sumx2 += xx**2 * cont;
                  stat_sumy2 += yy**2 * cont;
                  stat_sumz2 += zz**2 * cont;
               }
            }
         }
      }

      if (Math.abs(stat_sum0) > 1e-300) {
         res.meanx = stat_sumx1 / stat_sum0;
         res.meany = stat_sumy1 / stat_sum0;
         res.meanz = stat_sumz1 / stat_sum0;
         res.rmsx = Math.sqrt(Math.abs(stat_sumx2 / stat_sum0 - res.meanx**2));
         res.rmsy = Math.sqrt(Math.abs(stat_sumy2 / stat_sum0 - res.meany**2));
         res.rmsz = Math.sqrt(Math.abs(stat_sumz2 / stat_sum0 - res.meanz**2));
      }

      res.integral = stat_sum0;

      if (histo.fEntries > 1)
         res.entries = histo.fEntries;

      return res;
   }

   /** @summary Fill statistic */
   fillStatistic(stat, dostat /*, dofit */) {
      const data = this.countStat(),
          print_name = dostat % 10,
          print_entries = Math.floor(dostat / 10) % 10,
          print_mean = Math.floor(dostat / 100) % 10,
          print_rms = Math.floor(dostat / 1000) % 10,
          // print_under = Math.floor(dostat / 10000) % 10,
          // print_over = Math.floor(dostat / 100000) % 10,
          print_integral = Math.floor(dostat / 1000000) % 10;
          // print_skew = Math.floor(dostat / 10000000) % 10;
          // print_kurt = Math.floor(dostat / 100000000) % 10;

      stat.clearStat();

      if (print_name > 0)
         stat.addText(data.name);

      if (print_entries > 0)
         stat.addText('Entries = ' + stat.format(data.entries, 'entries'));

      if (print_mean > 0) {
         stat.addText('Mean x = ' + stat.format(data.meanx));
         stat.addText('Mean y = ' + stat.format(data.meany));
         stat.addText('Mean z = ' + stat.format(data.meanz));
      }

      if (print_rms > 0) {
         stat.addText('Std Dev x = ' + stat.format(data.rmsx));
         stat.addText('Std Dev y = ' + stat.format(data.rmsy));
         stat.addText('Std Dev z = ' + stat.format(data.rmsz));
      }

      if (print_integral > 0)
         stat.addText('Integral = ' + stat.format(data.integral, 'entries'));


      return true;
   }

   /** @summary Provide text information (tooltips) for histogram bin */
   getBinTooltips(ix, iy, iz) {
      const lines = [], histo = this.getHisto();
      let dx = 1, dy = 1, dz = 1;

      if (this.isDisplayItem()) {
         dx = histo.stepx || 1;
         dy = histo.stepy || 1;
         dz = histo.stepz || 1;
      }

      lines.push(this.getObjectHint(),
                 `x = ${this.getAxisBinTip('x', ix, dx)}  xbin=${ix+1}`,
                 `y = ${this.getAxisBinTip('y', iy, dy)}  ybin=${iy+1}`,
                 `z = ${this.getAxisBinTip('z', iz, dz)}  zbin=${iz+1}`);

      const binz = histo.getBinContent(ix+1, iy+1, iz+1),
            lbl = 'entries = '+ ((dx > 1) || (dy > 1) || (dz > 1) ? '~' : '');
      if (binz === Math.round(binz))
         lines.push(lbl + binz);
      else
         lines.push(lbl + floatToString(binz, gStyle.fStatFormat));

      return lines;
   }

   /** @summary Try to draw 3D histogram as scatter plot
     * @desc If there are too many points, returns promise with false */
   async draw3DScatter(handle) {
      const histo = this.getHisto(),
            main = this.getFramePainter(),
            i1 = handle.i1, i2 = handle.i2, di = handle.stepi,
            j1 = handle.j1, j2 = handle.j2, dj = handle.stepj,
            k1 = handle.k1, k2 = handle.k2, dk = handle.stepk;

      if ((i2 <= i1) || (j2 <= j1) || (k2 <= k1))
         return true;

      // scale down factor if too large values
      const coef = (this.gmaxbin > 1000) ? 1000/this.gmaxbin : 1,
            content_lmt = Math.max(0, this.gminbin);
      let i, j, k, bin_content, numpixels = 0, sumz = 0;

      for (i = i1; i < i2; i += di) {
         for (j = j1; j < j2; j += dj) {
            for (k = k1; k < k2; k += dk) {
               bin_content = histo.getBinContent(i+1, j+1, k+1);
               sumz += bin_content;
               if (bin_content <= content_lmt) continue;
               numpixels += Math.round(bin_content*coef);
            }
         }
      }

      // too many pixels - use box drawing
      if (numpixels > (main.webgl ? 100000 : 30000))
         return false;

      const pnts = new PointsCreator(numpixels, main.webgl, main.size_x3d/200),
            bins = new Int32Array(numpixels),
            xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), zaxis = this.getAxis('z'),
            rnd = new TRandom(sumz);
      let nbin = 0;

      for (i = i1; i < i2; i += di) {
         for (j = j1; j < j2; j += dj) {
            for (k = k1; k < k2; k += dk) {
               bin_content = histo.getBinContent(i+1, j+1, k+1);
               if (bin_content <= content_lmt) continue;
               const num = Math.round(bin_content*coef);

               for (let n=0; n<num; ++n) {
                  const binx = xaxis.GetBinCoord(i + rnd.random()),
                      biny = yaxis.GetBinCoord(j + rnd.random()),
                      binz = zaxis.GetBinCoord(k + rnd.random());

                  // remember bin index for tooltip
                  bins[nbin++] = histo.getBin(i+1, j+1, k+1);

                  pnts.addPoint(main.grx(binx), main.gry(biny), main.grz(binz));
               }
            }
         }
      }

      return pnts.createPoints({ color: this.v7EvalColor('fill_color', 'red') }).then(mesh => {
         main.add3DMesh(mesh);

         mesh.bins = bins;
         mesh.painter = this;
         mesh.tip_color = 0x00FF00;

         mesh.tooltip = function(intersect) {
            const indx = Math.floor(intersect.index / this.nvertex);
            if ((indx < 0) || (indx >= this.bins.length)) return null;

            const p = this.painter,
                main = p.getFramePainter(),
                tip = p.get3DToolTip(this.bins[indx]);

            tip.x1 = main.grx(p.getAxis('x').GetBinLowEdge(tip.ix));
            tip.x2 = main.grx(p.getAxis('x').GetBinLowEdge(tip.ix+di));
            tip.y1 = main.gry(p.getAxis('y').GetBinLowEdge(tip.iy));
            tip.y2 = main.gry(p.getAxis('y').GetBinLowEdge(tip.iy+dj));
            tip.z1 = main.grz(p.getAxis('z').GetBinLowEdge(tip.iz));
            tip.z2 = main.grz(p.getAxis('z').GetBinLowEdge(tip.iz+dk));
            tip.color = this.tip_color;
            tip.opacity = 0.3;

            return tip;
         };

         return true;
      });
   }

   /** @summary Drawing of 3D histogram */
   draw3DBins(handle) {
      const main = this.getFramePainter();
      let fillcolor = this.v7EvalColor('fill_color', 'red'),
          buffer_size = 0, use_lambert = false,
          use_helper = false, use_colors = false, use_opacity = 1, use_scale = true,
          single_bin_verts, single_bin_norms,
          tipscale = 0.5;

      if (this.options.Sphere) {
         // drawing spheres
         tipscale = 0.4;
         use_lambert = true;
         if (this.options.Sphere === 11) use_colors = true;

         const geom = main.webgl ? new SphereGeometry(0.5, 16, 12) : new SphereGeometry(0.5, 8, 6);
         geom.applyMatrix4(new Matrix4().makeRotationX(Math.PI/2));
         geom.computeVertexNormals();

         const indx = geom.getIndex().array,
             pos = geom.getAttribute('position').array,
             norm = geom.getAttribute('normal').array;

         buffer_size = indx.length*3;
         single_bin_verts = new Float32Array(buffer_size);
         single_bin_norms = new Float32Array(buffer_size);

         for (let k=0; k<indx.length; ++k) {
            const iii = indx[k]*3;
            single_bin_verts[k*3] = pos[iii];
            single_bin_verts[k*3+1] = pos[iii+1];
            single_bin_verts[k*3+2] = pos[iii+2];
            single_bin_norms[k*3] = norm[iii];
            single_bin_norms[k*3+1] = norm[iii+1];
            single_bin_norms[k*3+2] = norm[iii+2];
         }
      } else {
         const indicies = Box3D.Indexes,
             normals = Box3D.Normals,
             vertices = Box3D.Vertices;

         buffer_size = indicies.length*3;
         single_bin_verts = new Float32Array(buffer_size);
         single_bin_norms = new Float32Array(buffer_size);

         for (let k = 0, nn = -3; k < indicies.length; ++k) {
            const vert = vertices[indicies[k]];
            single_bin_verts[k*3] = vert.x-0.5;
            single_bin_verts[k*3+1] = vert.y-0.5;
            single_bin_verts[k*3+2] = vert.z-0.5;

            if (k%6 === 0) nn+=3;
            single_bin_norms[k*3] = normals[nn];
            single_bin_norms[k*3+1] = normals[nn+1];
            single_bin_norms[k*3+2] = normals[nn+2];
         }
         use_helper = true;

         if (this.options.Box === 11) use_colors = true; else
         if (this.options.Box === 12) { use_colors = true; use_helper = false; } else
         if (this.options.Color) { use_colors = true; use_opacity = 0.5; use_scale = false; use_helper = false; use_lambert = true; }
      }

      if (use_scale)
         use_scale = (this.gminbin || this.gmaxbin) ? 1 / Math.max(Math.abs(this.gminbin), Math.abs(this.gmaxbin)) : 1;

      const histo = this.getHisto(),
            i1 = handle.i1, i2 = handle.i2, di = handle.stepi,
            j1 = handle.j1, j2 = handle.j2, dj = handle.stepj,
            k1 = handle.k1, k2 = handle.k2, dk = handle.stepk;
      let palette = null;

      if (use_colors) {
         palette = main.getHistPalette();
         this.createContour(main, palette);
      }

      if ((i2 <= i1) || (j2 <= j1) || (k2 <= k1))
         return true;

      let xaxis = this.getAxis('x'), yaxis = this.getAxis('y'), zaxis = this.getAxis('z');
      const scalex = (main.grx(xaxis.GetBinCoord(i2)) - main.grx(xaxis.GetBinCoord(i1))) / (i2 - i1) * di,
            scaley = (main.gry(yaxis.GetBinCoord(j2)) - main.gry(yaxis.GetBinCoord(j1))) / (j2 - j1) * dj,
            scalez = (main.grz(zaxis.GetBinCoord(k2)) - main.grz(zaxis.GetBinCoord(k1))) / (k2 - k1) * dk,
            cols_size = [];
      let nbins = 0, i, j, k, wei, bin_content, num_colors = 0, cols_sequence = [];

      for (i = i1; i < i2; i += di) {
         for (j = j1; j < j2; j += dj) {
            for (k = k1; k < k2; k += dk) {
               bin_content = histo.getBinContent(i+1, j+1, k+1);
               if (!this.options.Color && ((bin_content === 0) || (bin_content < this.gminbin))) continue;
               wei = use_scale ? Math.pow(Math.abs(bin_content*use_scale), 0.3333) : 1;
               if (wei < 1e-3) continue; // do not draw empty or very small bins

               nbins++;

               if (!use_colors) continue;

               const colindx = palette.getContourIndex(bin_content);
               if (colindx >= 0) {
                  if (cols_size[colindx] === undefined) {
                     cols_size[colindx] = 0;
                     cols_sequence[colindx] = num_colors++;
                  }
                  cols_size[colindx]+=1;
               } else
                  console.error(`not found color for value = ${bin_content}`);
            }
         }
      }

      if (!use_colors) {
         cols_size.push(nbins);
         num_colors = 1;
         cols_sequence = [0];
      }

      const cols_nbins = new Array(num_colors),
            bin_verts = new Array(num_colors),
            bin_norms = new Array(num_colors),
            bin_tooltips = new Array(num_colors),
            helper_kind = new Array(num_colors),
            helper_indexes = new Array(num_colors),  // helper_kind === 1, use original vertices
            helper_positions = new Array(num_colors);  // helper_kind === 2, all vertices copied into separate buffer

      for (let ncol = 0; ncol < cols_size.length; ++ncol) {
         if (!cols_size[ncol]) continue; // ignore dummy colors

         nbins = cols_size[ncol]; // how many bins with specified color
         const nseq = cols_sequence[ncol];

         cols_nbins[nseq] = 0; // counter for the filled bins

         helper_kind[nseq] = 0;

         // 1 - use same vertices to create helper, one can use maximal 64K vertices
         // 2 - all vertices copied into separate buffer
         if (use_helper)
            helper_kind[nseq] = (nbins * buffer_size / 3 > 0xFFF0) ? 2 : 1;

         bin_verts[nseq] = new Float32Array(nbins * buffer_size);
         bin_norms[nseq] = new Float32Array(nbins * buffer_size);
         bin_tooltips[nseq] = new Int32Array(nbins);

         if (helper_kind[nseq] === 1)
            helper_indexes[nseq] = new Uint16Array(nbins * Box3D.MeshSegments.length);

         if (helper_kind[nseq] === 2)
            helper_positions[nseq] = new Float32Array(nbins * Box3D.Segments.length * 3);
      }

      let binx, grx, biny, gry, binz, grz;
      xaxis = this.getAxis('x');
      yaxis = this.getAxis('y');
      zaxis = this.getAxis('z');

      for (i = i1; i < i2; i += di) {
         binx = xaxis.GetBinCenter(i+1); grx = main.grx(binx);
         for (j = j1; j < j2; j += dj) {
            biny = yaxis.GetBinCenter(j+1); gry = main.gry(biny);
            for (k = k1; k < k2; k +=dk) {
               bin_content = histo.getBinContent(i+1, j+1, k+1);
               if (!this.options.Color && ((bin_content === 0) || (bin_content < this.gminbin))) continue;

               wei = use_scale ? Math.pow(Math.abs(bin_content*use_scale), 0.3333) : 1;
               if (wei < 1e-3) continue; // do not show very small bins

               let nseq = 0;
               if (use_colors) {
                  const colindx = palette.getContourIndex(bin_content);
                  if (colindx < 0) continue;
                  nseq = cols_sequence[colindx];
               }

               nbins = cols_nbins[nseq];

               binz = zaxis.GetBinCenter(k+1); grz = main.grz(binz);

               // remember bin index for tooltip
               bin_tooltips[nseq][nbins] = histo.getBin(i+1, j+1, k+1);

               let vvv = nbins * buffer_size;
               const bin_v = bin_verts[nseq], bin_n = bin_norms[nseq];

               // Grab the coordinates and scale that are being assigned to each bin
               for (let vi = 0; vi < buffer_size; vi+=3, vvv+=3) {
                  bin_v[vvv] = grx + single_bin_verts[vi]*scalex*wei;
                  bin_v[vvv+1] = gry + single_bin_verts[vi+1]*scaley*wei;
                  bin_v[vvv+2] = grz + single_bin_verts[vi+2]*scalez*wei;

                  bin_n[vvv] = single_bin_norms[vi];
                  bin_n[vvv+1] = single_bin_norms[vi+1];
                  bin_n[vvv+2] = single_bin_norms[vi+2];
               }

               if (helper_kind[nseq] === 1) {
                  // reuse vertices created for the mesh
                  const helper_segments = Box3D.MeshSegments;
                  vvv = nbins * helper_segments.length;
                  const shift = Math.round(nbins * buffer_size/3),
                        helper_i = helper_indexes[nseq];
                  for (let n = 0; n < helper_segments.length; ++n)
                     helper_i[vvv+n] = shift + helper_segments[n];
               }

               if (helper_kind[nseq] === 2) {
                  const helper_segments = Box3D.Segments,
                        helper_p = helper_positions[nseq];
                  vvv = nbins * helper_segments.length * 3;
                  for (let n = 0; n < helper_segments.length; ++n, vvv += 3) {
                     const vert = Box3D.Vertices[helper_segments[n]];
                     helper_p[vvv] = grx + (vert.x-0.5)*scalex*wei;
                     helper_p[vvv+1] = gry + (vert.y-0.5)*scaley*wei;
                     helper_p[vvv+2] = grz + (vert.z-0.5)*scalez*wei;
                  }
               }

               cols_nbins[nseq] = nbins+1;
            }
         }
      }

      for (let ncol = 0; ncol < cols_size.length; ++ncol) {
         if (!cols_size[ncol]) continue; // ignore dummy colors

         const nseq = cols_sequence[ncol],
              // BufferGeometries that store geometry of all bins
              all_bins_buffgeom = new BufferGeometry();

         // Create mesh from bin buffergeometry
         all_bins_buffgeom.setAttribute('position', new BufferAttribute(bin_verts[nseq], 3));
         all_bins_buffgeom.setAttribute('normal', new BufferAttribute(bin_norms[nseq], 3));

         if (use_colors) fillcolor = palette.getColor(ncol);

         const material = use_lambert
                           ? new MeshLambertMaterial({ color: fillcolor, opacity: use_opacity, transparent: use_opacity < 1, vertexColors: false })
                           : new MeshBasicMaterial({ color: fillcolor, opacity: use_opacity, transparent: use_opacity < 1, vertexColors: false }),
               combined_bins = new Mesh(all_bins_buffgeom, material);

         combined_bins.bins = bin_tooltips[nseq];
         combined_bins.bins_faces = buffer_size/9;
         combined_bins.painter = this;

         combined_bins.scalex = tipscale*scalex;
         combined_bins.scaley = tipscale*scaley;
         combined_bins.scalez = tipscale*scalez;
         combined_bins.tip_color = 0x00FF00;
         combined_bins.use_scale = use_scale;

         combined_bins.tooltip = function(intersect) {
            const indx = Math.floor(intersect.faceIndex / this.bins_faces);
            if ((indx < 0) || (indx >= this.bins.length)) return null;

            const p = this.painter,
                  main = p.getFramePainter(),
                  tip = p.get3DToolTip(this.bins[indx]),
                  grx = main.grx(p.getAxis('x').GetBinCoord(tip.ix-0.5)),
                  gry = main.gry(p.getAxis('y').GetBinCoord(tip.iy-0.5)),
                  grz = main.grz(p.getAxis('z').GetBinCoord(tip.iz-0.5)),
                  wei = this.use_scale ? Math.pow(Math.abs(tip.value*this.use_scale), 0.3333) : 1;

            tip.x1 = grx - this.scalex*wei; tip.x2 = grx + this.scalex*wei;
            tip.y1 = gry - this.scaley*wei; tip.y2 = gry + this.scaley*wei;
            tip.z1 = grz - this.scalez*wei; tip.z2 = grz + this.scalez*wei;

            tip.color = this.tip_color;

            return tip;
         };

         main.add3DMesh(combined_bins);

         if (helper_kind[nseq] > 0) {
            const lcolor = this.v7EvalColor('line_color', 'lightblue'),
                  helper_material = new LineBasicMaterial({ color: lcolor }),
                  lines = (helper_kind[nseq] === 1)
                          // reuse positions from the mesh - only special index was created
                          ? createLineSegments(bin_verts[nseq], helper_material, helper_indexes[nseq])
                          : createLineSegments(helper_positions[nseq], helper_material);

            main.add3DMesh(lines);
         }
      }

      if (use_colors)
         this.updatePaletteDraw();
   }

   draw3D() {
      if (!this.draw_content)
         return false;

      // this.options.Scatter = false;
      // this.options.Box = true;

      const handle = this.prepareDraw({ only_indexes: true, extra: -0.5, right_extra: -1 }),
            pr = this.options.Scatter ? this.draw3DScatter(handle) : Promise.resolve(false);

      return pr.then(res => {
         return res || this.draw3DBins(handle);
      });
   }

   /** @summary Redraw histogram */
   redraw(reason) {
      const main = this.getFramePainter(); // who makes axis and 3D drawing

      if (reason === 'resize') {
         if (main.resize3D()) main.render3D();
         return this;
      }

      assignFrame3DMethods(main);
      return main.create3DScene(this.options.Render3D).then(() => {
         main.setAxesRanges(this.getAxis('x'), this.xmin, this.xmax, this.getAxis('y'), this.ymin, this.ymax, this.getAxis('z'), this.zmin, this.zmax);
         main.set3DOptions(this.options);
         main.drawXYZ(main.toplevel, RAxisPainter, { zoom: settings.Zooming, ndim: 3, draw: true, v7: true });
         return this.drawingBins(reason);
      }).then(() => this.draw3D()).then(() => {
         main.render3D();
         main.addKeysHandler();
         return this;
      });
   }

   /** @summary Fill pad toolbar with RH3-related functions */
   fillToolbar() {
      const pp = this.getPadPainter();
      if (!pp) return;

      pp.addPadButton('auto_zoom', 'Unzoom all axes', 'ToggleZoom', 'Ctrl *');
      if (this.draw_content)
         pp.addPadButton('statbox', 'Toggle stat box', 'ToggleStatBox');
      pp.showPadButtons();
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range */
   canZoomInside(axis, min, max) {
      let obj = this.getHisto();
      if (obj) obj = obj['f'+axis.toUpperCase()+'axis'];
      return !obj || (obj.FindBin(max, 0.5) - obj.FindBin(min, 0) > 1);
   }

   /** @summary Perform automatic zoom inside non-zero region of histogram */
   autoZoom() {
      const i1 = this.getSelectIndex('x', 'left'),
            i2 = this.getSelectIndex('x', 'right'),
            j1 = this.getSelectIndex('y', 'left'),
            j2 = this.getSelectIndex('y', 'right'),
            k1 = this.getSelectIndex('z', 'left'),
            k2 = this.getSelectIndex('z', 'right'),
            histo = this.getHisto();
      let i, j, k;

      if ((i1 === i2) || (j1 === j2) || (k1 === k2)) return;

      // first find minimum
      let min = histo.getBinContent(i1 + 1, j1 + 1, k1+1);
      for (i = i1; i < i2; ++i) {
         for (j = j1; j < j2; ++j) {
            for (k = k1; k < k2; ++k)
               min = Math.min(min, histo.getBinContent(i+1, j+1, k+1));
         }
      }

      if (min > 0) return; // if all points positive, no chance for autoscale

      let ileft = i2, iright = i1, jleft = j2, jright = j1, kleft = k2, kright = k1;

      for (i = i1; i < i2; ++i) {
         for (j = j1; j < j2; ++j) {
            for (k = k1; k < k2; ++k) {
               if (histo.getBinContent(i+1, j+1, k+1) > min) {
                  if (i < ileft) ileft = i;
                  if (i >= iright) iright = i + 1;
                  if (j < jleft) jleft = j;
                  if (j >= jright) jright = j + 1;
                  if (k < kleft) kleft = k;
                  if (k >= kright) kright = k + 1;
               }
            }
         }
      }

      let xmin, xmax, ymin, ymax, zmin, zmax, isany = false;

      if ((ileft === iright-1) && (ileft > i1+1) && (iright < i2-1)) { ileft--; iright++; }
      if ((jleft === jright-1) && (jleft > j1+1) && (jright < j2-1)) { jleft--; jright++; }
      if ((kleft === kright-1) && (kleft > k1+1) && (kright < k2-1)) { kleft--; kright++; }

      if ((ileft > i1 || iright < i2) && (ileft < iright - 1)) {
         xmin = this.getAxis('x').GetBinLowEdge(ileft+1);
         xmax = this.getAxis('x').GetBinLowEdge(iright+1);
         isany = true;
      }

      if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) {
         ymin = this.getAxis('y').GetBinLowEdge(jleft+1);
         ymax = this.getAxis('y').GetBinLowEdge(jright+1);
         isany = true;
      }

      if ((kleft > k1 || kright < k2) && (kleft < kright - 1)) {
         zmin = this.getAxis('z').GetBinLowEdge(kleft+1);
         zmax = this.getAxis('z').GetBinLowEdge(kright+1);
         isany = true;
      }

      if (isany)
         return this.getFramePainter().zoom(xmin, xmax, ymin, ymax, zmin, zmax);
   }

   /** @summary Fill histogram context menu */
   fillHistContextMenu(menu) {
      const opts = this.getSupportedDrawOptions();

      menu.addDrawMenu('Draw with', opts, arg => {
         if (arg.indexOf(kInspect) === 0)
            return this.showInspector(arg);

         this.decodeOptions(arg);

         this.interactiveRedraw(true, 'drawopt');
      });
   }

   /** @summary draw RH3 object */
  static async draw(dom, histo /* ,opt */) {
      const painter = new RH3Painter(dom, histo);
      painter.mode3d = true;

      return ensureRCanvas(painter, '3d').then(() => {
         painter.setAsMainPainter();

         painter.options = { Box: 0, Scatter: false, Sphere: 0, Color: false, minimum: kNoZoom, maximum: kNoZoom, FrontBox: false, BackBox: false };

         const kind = painter.v7EvalAttr('kind', ''),
               sub = painter.v7EvalAttr('sub', 0),
               o = painter.options;

         switch (kind) {
            case 'box': o.Box = 10 + sub; break;
            case 'sphere': o.Sphere = 10 + sub; break;
            case 'col': o.Color = true; break;
            case 'scat': o.Scatter = true; break;
            default: o.Box = 10;
         }

         painter.scanContent();
         return painter.redraw();
      });
   }

} // class RH3Painter

/** @summary draw RHistDisplayItem  object
  * @private */
function drawHistDisplayItem(dom, obj, opt) {
   if (!obj)
      return null;

   if (obj.fAxes.length === 1)
      return RH1Painter.draw(dom, obj, opt);

   if (obj.fAxes.length === 2)
      return RH2Painter.draw(dom, obj, opt);

   if (obj.fAxes.length === 3)
      return RH3Painter.draw(dom, obj, opt);

   return null;
}

export { RH3Painter, drawHistDisplayItem };