hist/TGraph2DPainter.mjs

import { settings, createHistogram, setHistogramTitle, kNoZoom,
         clTH2F, clTGraph2DErrors, clTGraph2DAsymmErrors, clTPaletteAxis, kNoStats } from '../core.mjs';
import { Color, DoubleSide, LineBasicMaterial, MeshBasicMaterial, Mesh } from '../three.mjs';
import { DrawOptions } from '../base/BasePainter.mjs';
import { ObjectPainter } from '../base/ObjectPainter.mjs';
import { TH2Painter } from './TH2Painter.mjs';
import { Triangles3DHandler } from '../hist2d/TH2Painter.mjs';
import { createLineSegments, PointsCreator, getMaterialArgs } from '../base/base3d.mjs';
import { convertLegoBuf, createLegoGeom } from './hist3d.mjs';

function getMax(arr) {
   let v = arr[0];
   for (let i = 1; i < arr.length; ++i)
      v = Math.max(v, arr[i]);
   return v;
}

function getMin(arr) {
   let v = arr[0];
   for (let i = 1; i < arr.length; ++i)
      v = Math.min(v, arr[i]);
   return v;
}

function TMath_Sort(np, values, indicies /*, down */) {
   const arr = new Array(np);
   for (let i = 0; i < np; ++i)
      arr[i] = { v: values[i], i };

   arr.sort((a, b) => { return a.v < b.v ? -1 : (a.v > b.v ? 1 : 0); });

   for (let i = 0; i < np; ++i)
      indicies[i] = arr[i].i;
}

class TGraphDelaunay {

   constructor(g) {
      this.fGraph2D = g;
      this.fX = g.fX;
      this.fY = g.fY;
      this.fZ = g.fZ;
      this.fNpoints = g.fNpoints;
      this.fZout = 0.0;
      this.fNdt = 0;
      this.fNhull = 0;
      this.fHullPoints = null;
      this.fXN = null;
      this.fYN = null;
      this.fOrder = null;
      this.fDist = null;
      this.fPTried = null;
      this.fNTried = null;
      this.fMTried = null;
      this.fInit = false;
      this.fXNmin = 0.0;
      this.fXNmax = 0.0;
      this.fYNmin = 0.0;
      this.fYNmax = 0.0;
      this.fXoffset = 0.0;
      this.fYoffset = 0.0;
      this.fXScaleFactor = 0.0;
      this.fYScaleFactor = 0.0;

      this.SetMaxIter();
   }


   Initialize() {
      if (!this.fInit) {
         this.CreateTrianglesDataStructure();
         this.FindHull();
         this.fInit = true;
      }
   }

   ComputeZ(x, y) {
      // Initialise the Delaunay algorithm if needed.
      // CreateTrianglesDataStructure computes fXoffset, fYoffset,
      // fXScaleFactor and fYScaleFactor;
      // needed in this function.
      this.Initialize();

      // Find the z value corresponding to the point (x,y).
      const xx = (x+this.fXoffset)*this.fXScaleFactor,
            yy = (y+this.fYoffset)*this.fYScaleFactor;
      let zz = this.Interpolate(xx, yy);

      // Wrong zeros may appear when points sit on a regular grid.
      // The following line try to avoid this problem.
      if (zz === 0) zz = this.Interpolate(xx+0.0001, yy);

      return zz;
   }


   CreateTrianglesDataStructure() {
      // Offset fX and fY so they average zero, and scale so the average
      // of the X and Y ranges is one. The normalized version of fX and fY used
      // in Interpolate.
      const xmax = getMax(this.fGraph2D.fX),
            ymax = getMax(this.fGraph2D.fY),
            xmin = getMin(this.fGraph2D.fX),
            ymin = getMin(this.fGraph2D.fY);
      this.fXoffset = -(xmax+xmin)/2;
      this.fYoffset = -(ymax+ymin)/2;
      this.fXScaleFactor = 1/(xmax-xmin);
      this.fYScaleFactor = 1/(ymax-ymin);
      this.fXNmax = (xmax+this.fXoffset)*this.fXScaleFactor;
      this.fXNmin = (xmin+this.fXoffset)*this.fXScaleFactor;
      this.fYNmax = (ymax+this.fYoffset)*this.fYScaleFactor;
      this.fYNmin = (ymin+this.fYoffset)*this.fYScaleFactor;
      this.fXN = new Array(this.fNpoints+1);
      this.fYN = new Array(this.fNpoints+1);
      for (let n = 0; n < this.fNpoints; n++) {
         this.fXN[n+1] = (this.fX[n]+this.fXoffset)*this.fXScaleFactor;
         this.fYN[n+1] = (this.fY[n]+this.fYoffset)*this.fYScaleFactor;
      }

      // If needed, creates the arrays to hold the Delaunay triangles.
      // A maximum number of 2*fNpoints is guessed. If more triangles will be
      // find, FillIt will automatically enlarge these arrays.
      this.fPTried = [];
      this.fNTried = [];
      this.fMTried = [];
   }


   /// Is point e inside the triangle t1-t2-t3 ?

   Enclose(t1, t2, t3, e) {
      const x = [this.fXN[t1], this.fXN[t2], this.fXN[t3], this.fXN[t1]],
            y = [this.fYN[t1], this.fYN[t2], this.fYN[t3], this.fYN[t1]],
            xp = this.fXN[e],
            yp = this.fYN[e];
      let i = 0, j = x.length - 1, oddNodes = false;

      for (; i < x.length; ++i) {
         if ((y[i]<yp && y[j]>=yp) || (y[j]<yp && y[i]>=yp)) {
            if (x[i]+(yp-y[i])/(y[j]-y[i])*(x[j]-x[i])<xp)
               oddNodes = !oddNodes;
         }
         j = i;
      }

      return oddNodes;
   }


   /// Files the triangle defined by the 3 vertices p, n and m into the
   /// fxTried arrays. If these arrays are to small they are automatically
   /// expanded.

   FileIt(p, n, m) {
      let swap, tmp, ps = p, ns = n, ms = m;

      // order the vertices before storing them
      do {
         swap = false;
         if (ns > ps) { tmp = ps; ps = ns; ns = tmp; swap = true; }
         if (ms > ns) { tmp = ns; ns = ms; ms = tmp; swap = true; }
      } while (swap);

      // store a new Delaunay triangle
      this.fNdt++;
      this.fPTried.push(ps);
      this.fNTried.push(ns);
      this.fMTried.push(ms);
   }


   /// Attempt to find all the Delaunay triangles of the point set. It is not
   /// guaranteed that it will fully succeed, and no check is made that it has
   /// fully succeeded (such a check would be possible by referencing the points
   /// that make up the convex hull). The method is to check if each triangle
   /// shares all three of its sides with other triangles. If not, a point is
   /// generated just outside the triangle on the side(s) not shared, and a new
   /// triangle is found for that point. If this method is not working properly
   /// (many triangles are not being found) it's probably because the new points
   /// are too far beyond or too close to the non-shared sides. Fiddling with
   /// the size of the `alittlebit' parameter may help.

   FindAllTriangles() {
      if (this.fAllTri) return;

      this.fAllTri = true;

      let xcntr, ycntr, xm, ym, xx, yy,
          sx, sy, nx, ny, mx, my, mdotn, nn, a,
          t1, t2, pa, na, ma, pb, nb, mb, p1=0, p2=0, m, n, p3=0;
      const s = [false, false, false],
            alittlebit = 0.0001;

      this.Initialize();

      // start with a point that is guaranteed to be inside the hull (the
      // centre of the hull). The starting point is shifted "a little bit"
      // otherwise, in case of triangles aligned on a regular grid, we may
      // found none of them.
      xcntr = 0;
      ycntr = 0;
      for (n = 1; n <= this.fNhull; n++) {
         xcntr += this.fXN[this.fHullPoints[n-1]];
         ycntr += this.fYN[this.fHullPoints[n-1]];
      }
      xcntr = xcntr/this.fNhull+alittlebit;
      ycntr = ycntr/this.fNhull+alittlebit;
      // and calculate it's triangle
      this.Interpolate(xcntr, ycntr);

      // loop over all Delaunay triangles (including those constantly being
      // produced within the loop) and check to see if their 3 sides also
      // correspond to the sides of other Delaunay triangles, i.e. that they
      // have all their neighbours.
      t1 = 1;
      while (t1 <= this.fNdt) {
         // get the three points that make up this triangle
         pa = this.fPTried[t1-1];
         na = this.fNTried[t1-1];
         ma = this.fMTried[t1-1];

         // produce three integers which will represent the three sides
         s[0] = false;
         s[1] = false;
         s[2] = false;
         // loop over all other Delaunay triangles
         for (t2=1; t2<=this.fNdt; t2++) {
            if (t2 !== t1) {
               // get the points that make up this triangle
               pb = this.fPTried[t2-1];
               nb = this.fNTried[t2-1];
               mb = this.fMTried[t2-1];
               // do triangles t1 and t2 share a side?
               if ((pa === pb && na === nb) || (pa === pb && na === mb) || (pa === nb && na === mb)) {
                  // they share side 1
                  s[0] = true;
               } else if ((pa === pb && ma === nb) || (pa === pb && ma === mb) || (pa === nb && ma === mb)) {
                  // they share side 2
                  s[1] = true;
               } else if ((na === pb && ma === nb) || (na === pb && ma === mb) || (na === nb && ma === mb)) {
                  // they share side 3
                  s[2] = true;
               }
            }
            // if t1 shares all its sides with other Delaunay triangles then
            // forget about it
            if (s[0] && s[1] && s[2]) continue;
         }
         // Looks like t1 is missing a neighbour on at least one side.
         // For each side, take a point a little bit beyond it and calculate
         // the Delaunay triangle for that point, this should be the triangle
         // which shares the side.
         for (m=1; m<=3; m++) {
            if (!s[m-1]) {
               // get the two points that make up this side
               if (m === 1) {
                  p1 = pa;
                  p2 = na;
                  p3 = ma;
               } else if (m === 2) {
                  p1 = pa;
                  p2 = ma;
                  p3 = na;
               } else if (m === 3) {
                  p1 = na;
                  p2 = ma;
                  p3 = pa;
               }
               // get the coordinates of the centre of this side
               xm = (this.fXN[p1]+this.fXN[p2])/2.0;
               ym = (this.fYN[p1]+this.fYN[p2])/2.0;
               // we want to add a little to these coordinates to get a point just
               // outside the triangle; (sx,sy) will be the vector that represents
               // the side
               sx = this.fXN[p1]-this.fXN[p2];
               sy = this.fYN[p1]-this.fYN[p2];
               // (nx,ny) will be the normal to the side, but don't know if it's
               // pointing in or out yet
               nx = sy;
               ny = -sx;
               nn = Math.sqrt(nx*nx+ny*ny);
               nx = nx/nn;
               ny = ny/nn;
               mx = this.fXN[p3]-xm;
               my = this.fYN[p3]-ym;
               mdotn = mx*nx+my*ny;
               if (mdotn > 0) {
                  // (nx,ny) is pointing in, we want it pointing out
                  nx = -nx;
                  ny = -ny;
               }
               // increase/decrease xm and ym a little to produce a point
               // just outside the triangle (ensuring that the amount added will
               // be large enough such that it won't be lost in rounding errors)
               a = Math.abs(Math.max(alittlebit*xm, alittlebit*ym));
               xx = xm+nx*a;
               yy = ym+ny*a;
               // try and find a new Delaunay triangle for this point
               this.Interpolate(xx, yy);

               // this side of t1 should now, hopefully, if it's not part of the
               // hull, be shared with a new Delaunay triangle just calculated by Interpolate
            }
         }
         t1++;
      }
   }

   /// Finds those points which make up the convex hull of the set. If the xy
   /// plane were a sheet of wood, and the points were nails hammered into it
   /// at the respective coordinates, then if an elastic band were stretched
   /// over all the nails it would form the shape of the convex hull. Those
   /// nails in contact with it are the points that make up the hull.

   FindHull() {
      if (!this.fHullPoints)
         this.fHullPoints = new Array(this.fNpoints);

      let nhull_tmp = 0;
      for (let n=1; n<=this.fNpoints; n++) {
         // if the point is not inside the hull of the set of all points
         // bar it, then it is part of the hull of the set of all points
         // including it
         const is_in = this.InHull(n, n);
         if (!is_in) {
            // cannot increment fNhull directly - InHull needs to know that
            // the hull has not yet been completely found
            nhull_tmp++;
            this.fHullPoints[nhull_tmp-1] = n;
         }
      }
      this.fNhull = nhull_tmp;
   }


   /// Is point e inside the hull defined by all points apart from x ?

   InHull(e, x) {
      let n1, n2, n, m, ntry,
          lastdphi, dd1, dd2, dx1, dx2, dx3, dy1, dy2, dy3,
          u, v, vNv1, vNv2, phi1, phi2, dphi,
          deTinhull = false;

      const xx = this.fXN[e],
            yy = this.fYN[e];

      if (this.fNhull > 0) {
         //  The hull has been found - no need to use any points other than
         //  those that make up the hull
         ntry = this.fNhull;
      } else {
         //  The hull has not yet been found, will have to try every point
         ntry = this.fNpoints;
      }

      //  n1 and n2 will represent the two points most separated by angle
      //  from point e. Initially the angle between them will be <180 degs.
      //  But subsequent points will increase the n1-e-n2 angle. If it
      //  increases above 180 degrees then point e must be surrounded by
      //  points - it is not part of the hull.
      n1 = 1;
      n2 = 2;
      if (n1 === x) {
         n1 = n2;
         n2++;
      } else if (n2 === x)
         n2++;


      //  Get the angle n1-e-n2 and set it to lastdphi
      dx1 = xx-this.fXN[n1];
      dy1 = yy-this.fYN[n1];
      dx2 = xx-this.fXN[n2];
      dy2 = yy-this.fYN[n2];
      phi1 = Math.atan2(dy1, dx1);
      phi2 = Math.atan2(dy2, dx2);
      dphi = (phi1-phi2)-(Math.floor((phi1-phi2)/(Math.PI*2))*Math.PI*2);
      if (dphi < 0) dphi += Math.PI*2;
      lastdphi = dphi;
      for (n=1; n<=ntry; n++) {
         if (this.fNhull > 0) {
            // Try hull point n
            m = this.fHullPoints[n-1];
         } else
            m = n;

         if ((m !== n1) && (m !== n2) && (m !== x)) {
            // Can the vector e->m be represented as a sum with positive
            // coefficients of vectors e->n1 and e->n2?
            dx1 = xx-this.fXN[n1];
            dy1 = yy-this.fYN[n1];
            dx2 = xx-this.fXN[n2];
            dy2 = yy-this.fYN[n2];
            dx3 = xx-this.fXN[m];
            dy3 = yy-this.fYN[m];

            dd1 = (dx2*dy1-dx1*dy2);
            dd2 = (dx1*dy2-dx2*dy1);

            if (dd1*dd2 !== 0) {
               u = (dx2*dy3-dx3*dy2)/dd1;
               v = (dx1*dy3-dx3*dy1)/dd2;
               if ((u < 0) || (v < 0)) {
                  // No, it cannot - point m does not lie in-between n1 and n2 as
                  // viewed from e. Replace either n1 or n2 to increase the
                  // n1-e-n2 angle. The one to replace is the one which makes the
                  // smallest angle with e->m
                  vNv1 = (dx1*dx3+dy1*dy3)/Math.sqrt(dx1*dx1+dy1*dy1);
                  vNv2 = (dx2*dx3+dy2*dy3)/Math.sqrt(dx2*dx2+dy2*dy2);
                  if (vNv1 > vNv2) {
                     n1 = m;
                     phi1 = Math.atan2(dy3, dx3);
                     phi2 = Math.atan2(dy2, dx2);
                  } else {
                     n2 = m;
                     phi1 = Math.atan2(dy1, dx1);
                     phi2 = Math.atan2(dy3, dx3);
                  }
                  dphi = (phi1-phi2)-(Math.floor((phi1-phi2)/(Math.PI*2))*Math.PI*2);
                  if (dphi < 0) dphi += Math.PI*2;
                  if ((dphi - Math.PI)*(lastdphi - Math.PI) < 0) {
                     // The addition of point m means the angle n1-e-n2 has risen
                     // above 180 degs, the point is in the hull.
                     deTinhull = true;
                     return deTinhull;
                  }
                  lastdphi = dphi;
               }
            }
         }
      }
      // Point e is not surrounded by points - it is not in the hull.
      return deTinhull;
   }


   /// Finds the z-value at point e given that it lies
   /// on the plane defined by t1,t2,t3

   InterpolateOnPlane(TI1, TI2, TI3, e) {
      let tmp, swap, t1 = TI1, t2 = TI2, t3 = TI3;

      // order the vertices
      do {
         swap = false;
         if (t2 > t1) { tmp = t1; t1 = t2; t2 = tmp; swap = true; }
         if (t3 > t2) { tmp = t2; t2 = t3; t3 = tmp; swap = true; }
      } while (swap);

      const x1 = this.fXN[t1],
            x2 = this.fXN[t2],
            x3 = this.fXN[t3],
            y1 = this.fYN[t1],
            y2 = this.fYN[t2],
            y3 = this.fYN[t3],
            f1 = this.fZ[t1-1],
            f2 = this.fZ[t2-1],
            f3 = this.fZ[t3-1],
            u = (f1*(y2-y3)+f2*(y3-y1)+f3*(y1-y2))/(x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2)),
            v = (f1*(x2-x3)+f2*(x3-x1)+f3*(x1-x2))/(y1*(x2-x3)+y2*(x3-x1)+y3*(x1-x2)),
            w = f1-u*x1-v*y1;

      return u*this.fXN[e] + v*this.fYN[e] + w;
   }

   /// Finds the Delaunay triangle that the point (xi,yi) sits in (if any) and
   /// calculate a z-value for it by linearly interpolating the z-values that
   /// make up that triangle.

   Interpolate(xx, yy) {
      let thevalue,
          it, ntris_tried, p, n, m,
          i, j, k, l, z, f, d, o1, o2, a, b, t1, t2, t3,
          ndegen = 0, degen = 0, fdegen = 0, o1degen = 0, o2degen = 0,
          vxN, vyN,
          d1, d2, d3, c1, c2, dko1, dko2, dfo1,
          dfo2, sin_sum, cfo1k, co2o1k, co2o1f,
          dx1, dx2, dx3, dy1, dy2, dy3, u, v;
      const dxz = [0, 0, 0], dyz = [0, 0, 0];

      // initialise the Delaunay algorithm if needed
      this.Initialize();

      // create vectors needed for sorting
      if (!this.fOrder) {
         this.fOrder = new Array(this.fNpoints);
         this.fDist = new Array(this.fNpoints);
      }

      // the input point will be point zero.
      this.fXN[0] = xx;
      this.fYN[0] = yy;

      // set the output value to the default value for now
      thevalue = this.fZout;

      // some counting
      ntris_tried = 0;

      // no point in proceeding if xx or yy are silly
      if ((xx > this.fXNmax) || (xx < this.fXNmin) || (yy > this.fYNmax) || (yy < this.fYNmin))
         return thevalue;

      // check existing Delaunay triangles for a good one
      for (it=1; it<=this.fNdt; it++) {
         p = this.fPTried[it-1];
         n = this.fNTried[it-1];
         m = this.fMTried[it-1];
         // p, n and m form a previously found Delaunay triangle, does it
         // enclose the point?
         if (this.Enclose(p, n, m, 0)) {
            // yes, we have the triangle
            thevalue = this.InterpolateOnPlane(p, n, m, 0);
            return thevalue;
         }
      }

      // is this point inside the convex hull?
      const shouldbein = this.InHull(0, -1);
      if (!shouldbein)
         return thevalue;

      // it must be in a Delaunay triangle - find it...

      // order mass points by distance in mass plane from desired point
      for (it=1; it<=this.fNpoints; it++) {
         vxN = this.fXN[it];
         vyN = this.fYN[it];
         this.fDist[it-1] = Math.sqrt((xx-vxN)*(xx-vxN)+(yy-vyN)*(yy-vyN));
      }

      // sort array 'fDist' to find closest points
      TMath_Sort(this.fNpoints, this.fDist, this.fOrder /*, false */);
      for (it=0; it<this.fNpoints; it++) this.fOrder[it]++;

      // loop over triplets of close points to try to find a triangle that
      // encloses the point.
      for (k=3; k<=this.fNpoints; k++) {
         m = this.fOrder[k-1];
         for (j=2; j<=k-1; j++) {
            n = this.fOrder[j-1];
            for (i=1; i<=j-1; i++) {
               let skip_this_triangle = false; // used instead of goto L90
               p = this.fOrder[i-1];
               if (ntris_tried > this.fMaxIter) {
                  // perhaps this point isn't in the hull after all
   ///            Warning("Interpolate",
   ///                    "Abandoning the effort to find a Delaunay triangle (and thus interpolated z-value) for point %g %g"
   ///                    ,xx,yy);
                  return thevalue;
               }
               ntris_tried++;
               // check the points aren't colinear
               d1 = Math.sqrt((this.fXN[p]-this.fXN[n])**2+(this.fYN[p]-this.fYN[n])**2);
               d2 = Math.sqrt((this.fXN[p]-this.fXN[m])**2+(this.fYN[p]-this.fYN[m])**2);
               d3 = Math.sqrt((this.fXN[n]-this.fXN[m])**2+(this.fYN[n]-this.fYN[m])**2);
               if ((d1+d2 <= d3) || (d1+d3 <= d2) || (d2+d3 <= d1))
                  continue;

               // does the triangle enclose the point?
               if (!this.Enclose(p, n, m, 0))
                  continue;

               // is it a Delaunay triangle? (ie. are there any other points
               // inside the circle that is defined by its vertices?)

               // test the triangle for Delaunay'ness

               // loop over all other points testing each to see if it's
               // inside the triangle's circle
               ndegen = 0;
               for (z = 1; z <= this.fNpoints; z++) {
                  if ((z === p) || (z === n) || (z === m))
                     continue; // goto L50;
                  // An easy first check is to see if point z is inside the triangle
                  // (if it's in the triangle it's also in the circle)

                  // point z cannot be inside the triangle if it's further from (xx,yy)
                  // than the furthest pointing making up the triangle - test this
                  for (l=1; l<=this.fNpoints; l++) {
                     if (this.fOrder[l-1] === z) {
                        if ((l<i) || (l<j) || (l<k)) {
                           // point z is nearer to (xx,yy) than m, n or p - it could be in the
                           // triangle so call enclose to find out

                           // if it is inside the triangle this can't be a Delaunay triangle
                           if (this.Enclose(p, n, m, z)) { skip_this_triangle = true; break; } // goto L90;
                        } else {
                           // there's no way it could be in the triangle so there's no point
                           // calling enclose
                           break; // goto L1;
                        }
                     }
                  }

                  if (skip_this_triangle) break;

                  // is point z colinear with any pair of the triangle points?
   // L1:
                  if (((this.fXN[p]-this.fXN[z])*(this.fYN[p]-this.fYN[n])) === ((this.fYN[p]-this.fYN[z])*(this.fXN[p]-this.fXN[n]))) {
                     // z is colinear with p and n
                     a = p;
                     b = n;
                  } else if (((this.fXN[p]-this.fXN[z])*(this.fYN[p]-this.fYN[m])) === ((this.fYN[p]-this.fYN[z])*(this.fXN[p]-this.fXN[m]))) {
                     // z is colinear with p and m
                     a = p;
                     b = m;
                  } else if (((this.fXN[n]-this.fXN[z])*(this.fYN[n]-this.fYN[m])) === ((this.fYN[n]-this.fYN[z])*(this.fXN[n]-this.fXN[m]))) {
                     // z is colinear with n and m
                     a = n;
                     b = m;
                  } else {
                     a = 0;
                     b = 0;
                  }
                  if (a !== 0) {
                     // point z is colinear with 2 of the triangle points, if it lies
                     // between them it's in the circle otherwise it's outside
                     if (this.fXN[a] !== this.fXN[b]) {
                        if (((this.fXN[z]-this.fXN[a])*(this.fXN[z]-this.fXN[b])) < 0) {
                           skip_this_triangle = true;
                           break;
                           // goto L90;
                        } else if (((this.fXN[z]-this.fXN[a])*(this.fXN[z]-this.fXN[b])) === 0) {
                           // At least two points are sitting on top of each other, we will
                           // treat these as one and not consider this a 'multiple points lying
                           // on a common circle' situation. It is a sign something could be
                           // wrong though, especially if the two coincident points have
                           // different fZ's. If they don't then this is harmless.
                           console.warn(`Interpolate Two of these three points are coincident ${a} ${b} ${z}`);
                        }
                     } else {
                        if (((this.fYN[z]-this.fYN[a])*(this.fYN[z]-this.fYN[b])) < 0) {
                           skip_this_triangle = true;
                           break;
                           // goto L90;
                        } else if (((this.fYN[z]-this.fYN[a])*(this.fYN[z]-this.fYN[b])) === 0) {
                           // At least two points are sitting on top of each other - see above.
                           console.warn(`Interpolate Two of these three points are coincident ${a} ${b} ${z}`);
                        }
                     }
                     // point is outside the circle, move to next point
                     continue; // goto L50;
                  }

                  if (skip_this_triangle) break; // deepscan-disable-line

   ///            Error("Interpolate", "Should not get to here");
                  // may as well soldier on
                  // SL: initialize before try to find better values
                  f = m;
                  o1 = p;
                  o2 = n;

                  // if point z were to look at the triangle, which point would it see
                  // lying between the other two? (we're going to form a quadrilateral
                  // from the points, and then demand certain properties of that
                  // quadrilateral)
                  dxz[0] = this.fXN[p]-this.fXN[z];
                  dyz[0] = this.fYN[p]-this.fYN[z];
                  dxz[1] = this.fXN[n]-this.fXN[z];
                  dyz[1] = this.fYN[n]-this.fYN[z];
                  dxz[2] = this.fXN[m]-this.fXN[z];
                  dyz[2] = this.fYN[m]-this.fYN[z];
                  for (l=1; l<=3; l++) {
                     dx1 = dxz[l-1];
                     dx2 = dxz[l%3];
                     dx3 = dxz[(l+1)%3];
                     dy1 = dyz[l-1];
                     dy2 = dyz[l%3];
                     dy3 = dyz[(l+1)%3];

                     // u et v are used only to know their sign. The previous
                     // code computed them with a division which was long and
                     // might be a division by 0. It is now replaced by a
                     // multiplication.
                     u = (dy3*dx2-dx3*dy2)*(dy1*dx2-dx1*dy2);
                     v = (dy3*dx1-dx3*dy1)*(dy2*dx1-dx2*dy1);

                     if ((u >= 0) && (v >= 0)) {
                        // vector (dx3,dy3) is expressible as a sum of the other two vectors
                        // with positive coefficients -> i.e. it lies between the other two vectors
                        if (l === 1) {
                           f = m; o1 = p; o2 = n; // deepscan-disable-line
                        } else if (l === 2) {
                           f = p; o1 = n; o2 = m;
                        } else {
                           f = n; o1 = m; o2 = p;
                        }
                        break; // goto L2;
                     }
                  }
   // L2:
                  // this is not a valid quadrilateral if the diagonals don't cross,
                  // check that points f and z lie on opposite side of the line o1-o2,
                  // this is true if the angle f-o1-z is greater than o2-o1-z and o2-o1-f
                  cfo1k = ((this.fXN[f]-this.fXN[o1])*(this.fXN[z]-this.fXN[o1])+(this.fYN[f]-this.fYN[o1])*(this.fYN[z]-this.fYN[o1]))/
                           Math.sqrt(((this.fXN[f]-this.fXN[o1])*(this.fXN[f]-this.fXN[o1])+(this.fYN[f]-this.fYN[o1])*(this.fYN[f]-this.fYN[o1]))*
                           ((this.fXN[z]-this.fXN[o1])*(this.fXN[z]-this.fXN[o1])+(this.fYN[z]-this.fYN[o1])*(this.fYN[z]-this.fYN[o1])));
                  co2o1k = ((this.fXN[o2]-this.fXN[o1])*(this.fXN[z]-this.fXN[o1])+(this.fYN[o2]-this.fYN[o1])*(this.fYN[z]-this.fYN[o1]))/
                           Math.sqrt(((this.fXN[o2]-this.fXN[o1])*(this.fXN[o2]-this.fXN[o1])+(this.fYN[o2]-this.fYN[o1])*(this.fYN[o2]-this.fYN[o1]))*
                           ((this.fXN[z]-this.fXN[o1])*(this.fXN[z]-this.fXN[o1]) + (this.fYN[z]-this.fYN[o1])*(this.fYN[z]-this.fYN[o1])));
                  co2o1f = ((this.fXN[o2]-this.fXN[o1])*(this.fXN[f]-this.fXN[o1])+(this.fYN[o2]-this.fYN[o1])*(this.fYN[f]-this.fYN[o1]))/
                           Math.sqrt(((this.fXN[o2]-this.fXN[o1])*(this.fXN[o2]-this.fXN[o1])+(this.fYN[o2]-this.fYN[o1])*(this.fYN[o2]-this.fYN[o1]))*
                           ((this.fXN[f]-this.fXN[o1])*(this.fXN[f]-this.fXN[o1]) + (this.fYN[f]-this.fYN[o1])*(this.fYN[f]-this.fYN[o1])));
                  if ((cfo1k > co2o1k) || (cfo1k > co2o1f)) {
                     // not a valid quadrilateral - point z is definitely outside the circle
                     continue; // goto L50;
                  }
                  // calculate the 2 internal angles of the quadrangle formed by joining
                  // points z and f to points o1 and o2, at z and f. If they sum to less
                  // than 180 degrees then z lies outside the circle
                  dko1 = Math.sqrt((this.fXN[z]-this.fXN[o1])*(this.fXN[z]-this.fXN[o1])+(this.fYN[z]-this.fYN[o1])*(this.fYN[z]-this.fYN[o1]));
                  dko2 = Math.sqrt((this.fXN[z]-this.fXN[o2])*(this.fXN[z]-this.fXN[o2])+(this.fYN[z]-this.fYN[o2])*(this.fYN[z]-this.fYN[o2]));
                  dfo1 = Math.sqrt((this.fXN[f]-this.fXN[o1])*(this.fXN[f]-this.fXN[o1])+(this.fYN[f]-this.fYN[o1])*(this.fYN[f]-this.fYN[o1]));
                  dfo2 = Math.sqrt((this.fXN[f]-this.fXN[o2])*(this.fXN[f]-this.fXN[o2])+(this.fYN[f]-this.fYN[o2])*(this.fYN[f]-this.fYN[o2]));
                  c1 = ((this.fXN[z]-this.fXN[o1])*(this.fXN[z]-this.fXN[o2])+(this.fYN[z]-this.fYN[o1])*(this.fYN[z]-this.fYN[o2]))/dko1/dko2;
                  c2 = ((this.fXN[f]-this.fXN[o1])*(this.fXN[f]-this.fXN[o2])+(this.fYN[f]-this.fYN[o1])*(this.fYN[f]-this.fYN[o2]))/dfo1/dfo2;
                  sin_sum = c1*Math.sqrt(1-c2*c2)+c2*Math.sqrt(1-c1*c1);

                  // sin_sum doesn't always come out as zero when it should do.
                  if (sin_sum < -1.e-6) {
                     // z is inside the circle, this is not a Delaunay triangle
                     skip_this_triangle = true;
                     break;
                     // goto L90;
                  } else if (Math.abs(sin_sum) <= 1.e-6) {
                     // point z lies on the circumference of the circle (within rounding errors)
                     // defined by the triangle, so there is potential for degeneracy in the
                     // triangle set (Delaunay triangulation does not give a unique way to split
                     // a polygon whose points lie on a circle into constituent triangles). Make
                     // a note of the additional point number.
                     ndegen++;
                     degen = z;
                     fdegen = f;
                     o1degen = o1;
                     o2degen = o2;
                  }

                  // L50: continue;
               } // end of for ( z = 1 ...) loop

               if (skip_this_triangle) continue;

               // This is a good triangle
               if (ndegen > 0) {
                  // but is degenerate with at least one other,
                  // haven't figured out what to do if more than 4 points are involved
   ///            if (ndegen > 1) {
   ///               Error("Interpolate",
   ///                     "More than 4 points lying on a circle. No decision making process formulated for triangulating this region in a non-arbitrary way %d %d %d %d",
   ///                     p,n,m,degen);
   ///               return thevalue;
   ///            }

                  // we have a quadrilateral which can be split down either diagonal
                  // (d<->f or o1<->o2) to form valid Delaunay triangles. Choose diagonal
                  // with highest average z-value. Whichever we choose we will have
                  // verified two triangles as good and two as bad, only note the good ones
                  d = degen;
                  f = fdegen;
                  o1 = o1degen;
                  o2 = o2degen;
                  if ((this.fZ[o1-1] + this.fZ[o2-1]) > (this.fZ[d-1] + this.fZ[f-1])) {
                     // best diagonalisation of quadrilateral is current one, we have
                     // the triangle
                     t1 = p;
                     t2 = n;
                     t3 = m;
                     // file the good triangles
                     this.FileIt(p, n, m);
                     this.FileIt(d, o1, o2);
                  } else {
                     // use other diagonal to split quadrilateral, use triangle formed by
                     // point f, the degnerate point d and whichever of o1 and o2 create
                     // an enclosing triangle
                     t1 = f;
                     t2 = d;
                     if (this.Enclose(f, d, o1, 0))
                        t3 = o1;
                      else
                        t3 = o2;

                     // file the good triangles
                     this.FileIt(f, d, o1);
                     this.FileIt(f, d, o2);
                  }
               } else {
                  // this is a Delaunay triangle, file it
                  this.FileIt(p, n, m);
                  t1 = p;
                  t2 = n;
                  t3 = m;
               }
               // do the interpolation
               thevalue = this.InterpolateOnPlane(t1, t2, t3, 0);
               return thevalue;

   // L90:      continue;
            }
         }
      }
      if (shouldbein) // deepscan-disable-line
         console.error(`Interpolate Point outside hull when expected inside: this point could be dodgy ${xx}  ${yy} ${ntris_tried}`);
      return thevalue;
   }

   /// Defines the number of triangles tested for a Delaunay triangle
   /// (number of iterations) before abandoning the search

   SetMaxIter(n = 100000) {
      this.fAllTri = false;
      this.fMaxIter = n;
   }

   /// Sets the histogram bin height for points lying outside the convex hull ie:
   /// the bins in the margin.

   SetMarginBinsContent(z) {
      this.fZout = z;
   }

}

   /** @summary Function handles tooltips in the mesh */
function graph2DTooltip(intersect) {
   let indx = Math.floor(intersect.index / this.nvertex);
   if ((indx < 0) || (indx >= this.index.length)) return null;
   const sqr = v => v*v;

   indx = this.index[indx];

   const fp = this.fp, gr = this.graph;
   let grx = fp.grx(gr.fX[indx]),
       gry = fp.gry(gr.fY[indx]),
       grz = fp.grz(gr.fZ[indx]);

   if (this.check_next && indx+1<gr.fX.length) {
      const d = intersect.point,
          grx1 = fp.grx(gr.fX[indx+1]),
          gry1 = fp.gry(gr.fY[indx+1]),
          grz1 = fp.grz(gr.fZ[indx+1]);
      if (sqr(d.x-grx1)+sqr(d.y-gry1)+sqr(d.z-grz1) < sqr(d.x-grx)+sqr(d.y-gry)+sqr(d.z-grz)) {
         grx = grx1; gry = gry1; grz = grz1; indx++;
      }
   }

   return {
      x1: grx - this.scale0,
      x2: grx + this.scale0,
      y1: gry - this.scale0,
      y2: gry + this.scale0,
      z1: grz - this.scale0,
      z2: grz + this.scale0,
      color: this.tip_color,
      lines: [this.tip_name,
               'pnt: ' + indx,
               'x: ' + fp.axisAsText('x', gr.fX[indx]),
               'y: ' + fp.axisAsText('y', gr.fY[indx]),
               'z: ' + fp.axisAsText('z', gr.fZ[indx])
             ]
   };
}



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

class TGraph2DPainter extends ObjectPainter {

   /** @summary Decode options string  */
   decodeOptions(opt, _gr) {
      const d = new DrawOptions(opt);

      if (!this.options)
         this.options = {};

      const res = this.options, gr2d = this.getObject();

      if (d.check('FILL_', 'color') && gr2d)
         gr2d.fFillColor = d.color;

      if (d.check('LINE_', 'color') && gr2d)
         gr2d.fLineColor = d.color;

      d.check('SAME');
      if (d.check('TRI1'))
         res.Triangles = 11; // wireframe and colors
      else if (d.check('TRI2'))
         res.Triangles = 10; // only color triangles
      else if (d.check('TRIW'))
         res.Triangles = 1;
      else if (d.check('TRI'))
         res.Triangles = 2;
      else
         res.Triangles = 0;
      res.Line = d.check('LINE');
      res.Error = d.check('ERR') && (this.matchObjectType(clTGraph2DErrors) || this.matchObjectType(clTGraph2DAsymmErrors));

      if (d.check('P0COL'))
         res.Color = res.Circles = res.Markers = true;
       else {
         res.Color = d.check('COL');
         res.Circles = d.check('P0');
         res.Markers = d.check('P');
      }

      if (!res.Markers) res.Color = false;

      if (res.Color || res.Triangles >= 10)
         res.Zscale = d.check('Z');

      res.isAny = function() {
         return this.Markers || this.Error || this.Circles || this.Line || this.Triangles;
      };

      if (res.isAny()) {
         res.Axis = 'lego2';
         if (res.Zscale) res.Axis += 'z';
      } else
         res.Axis = opt;

      this.storeDrawOpt(opt);
   }

   /** @summary Create histogram for axes drawing */
   createHistogram() {
      const gr = this.getObject(),
            asymm = this.matchObjectType(clTGraph2DAsymmErrors);
      let xmin = gr.fX[0], xmax = xmin,
          ymin = gr.fY[0], ymax = ymin,
          zmin = gr.fZ[0], zmax = zmin;

      for (let p = 0; p < gr.fNpoints; ++p) {
         const x = gr.fX[p], y = gr.fY[p], z = gr.fZ[p];

         if (this.options.Error) {
            xmin = Math.min(xmin, x - (asymm ? gr.fEXlow[p] : gr.fEX[p]));
            xmax = Math.max(xmax, x + (asymm ? gr.fEXhigh[p] : gr.fEX[p]));
            ymin = Math.min(ymin, y - (asymm ? gr.fEYlow[p] : gr.fEY[p]));
            ymax = Math.max(ymax, y + (asymm ? gr.fEYhigh[p] : gr.fEY[p]));
            zmin = Math.min(zmin, z - (asymm ? gr.fEZlow[p] : gr.fEZ[p]));
            zmax = Math.max(zmax, z + (asymm ? gr.fEZhigh[p] : gr.fEZ[p]));
         } else {
            xmin = Math.min(xmin, x);
            xmax = Math.max(xmax, x);
            ymin = Math.min(ymin, y);
            ymax = Math.max(ymax, y);
            zmin = Math.min(zmin, z);
            zmax = Math.max(zmax, z);
         }
      }

      function calc_delta(min, max, margin) {
         if (min < max) return margin * (max - min);
         return Math.abs(min) < 1e5 ? 0.02 : 0.02 * Math.abs(min);
      }
      const dx = calc_delta(xmin, xmax, gr.fMargin),
            dy = calc_delta(ymin, ymax, gr.fMargin),
            dz = calc_delta(zmin, zmax, 0);
      let uxmin = xmin - dx, uxmax = xmax + dx,
          uymin = ymin - dy, uymax = ymax + dy,
          uzmin = zmin - dz, uzmax = zmax + dz;

      if ((uxmin < 0) && (xmin >= 0)) uxmin = xmin*0.98;
      if ((uxmax > 0) && (xmax <= 0)) uxmax = 0;

      if ((uymin < 0) && (ymin >= 0)) uymin = ymin*0.98;
      if ((uymax > 0) && (ymax <= 0)) uymax = 0;

      if ((uzmin < 0) && (zmin >= 0)) uzmin = zmin*0.98;
      if ((uzmax > 0) && (zmax <= 0)) uzmax = 0;

      const graph = this.getObject();

      if (graph.fMinimum !== kNoZoom) uzmin = graph.fMinimum;
      if (graph.fMaximum !== kNoZoom) uzmax = graph.fMaximum;

      this._own_histogram = true; // when histogram created on client side

      const histo = createHistogram(clTH2F, graph.fNpx, graph.fNpy);
      histo.fName = graph.fName + '_h';
      setHistogramTitle(histo, graph.fTitle);
      histo.fXaxis.fXmin = uxmin;
      histo.fXaxis.fXmax = uxmax;
      histo.fYaxis.fXmin = uymin;
      histo.fYaxis.fXmax = uymax;
      histo.fZaxis.fXmin = uzmin;
      histo.fZaxis.fXmax = uzmax;
      histo.fMinimum = uzmin;
      histo.fMaximum = uzmax;
      histo.fBits |= kNoStats;

      if (!this.options.isAny()) {
         const dulaunay = this.buildDelaunay(graph);
         if (dulaunay) {
            for (let i = 0; i < graph.fNpx; ++i) {
               const xx = uxmin + (i + 0.5) / graph.fNpx * (uxmax - uxmin);
               for (let j = 0; j < graph.fNpy; ++j) {
                  const yy = uymin + (j + 0.5) / graph.fNpy * (uymax - uymin),
                        zz = dulaunay.ComputeZ(xx, yy);
                  histo.fArray[histo.getBin(i+1, j+1)] = zz;
               }
            }
         }
      }

      return histo;
   }

   buildDelaunay(graph) {
      if (!this._delaunay) {
         this._delaunay = new TGraphDelaunay(graph);
         this._delaunay.FindAllTriangles();
         if (!this._delaunay.fNdt)
            delete this._delaunay;
      }
      return this._delaunay;
   }

   drawTriangles(fp, graph, levels, palette) {
      const dulaunay = this.buildDelaunay(graph);
      if (!dulaunay) return;

      const main_grz = !fp.logz ? fp.grz : value => (value < fp.scale_zmin) ? -0.1 : fp.grz(value),
            plain_mode = this.options.Triangles === 2,
            do_faces = (this.options.Triangles >= 10) || plain_mode,
            do_lines = (this.options.Triangles % 10 === 1) || (plain_mode && (graph.fLineColor !== graph.fFillColor)),
            triangles = new Triangles3DHandler(levels, main_grz, 0, 2*fp.size_z3d, do_lines);

      for (triangles.loop = 0; triangles.loop < 2; ++triangles.loop) {
         triangles.createBuffers();

         for (let t = 0; t < dulaunay.fNdt; ++t) {
            const points = [dulaunay.fPTried[t], dulaunay.fNTried[t], dulaunay.fMTried[t]],
                  coord = [];
            let use_triangle = true;
            for (let i = 0; i < 3; ++i) {
               const pnt = points[i] - 1;
               coord.push(fp.grx(graph.fX[pnt]), fp.gry(graph.fY[pnt]), main_grz(graph.fZ[pnt]));

                if ((graph.fX[pnt] < fp.scale_xmin) || (graph.fX[pnt] > fp.scale_xmax) ||
                    (graph.fY[pnt] < fp.scale_ymin) || (graph.fY[pnt] > fp.scale_ymax))
                  use_triangle = false;
            }

            if (do_faces && use_triangle)
               triangles.addMainTriangle(...coord);

            if (do_lines && use_triangle) {
               triangles.addLineSegment(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]);

               triangles.addLineSegment(coord[3], coord[4], coord[5], coord[6], coord[7], coord[8]);

               triangles.addLineSegment(coord[6], coord[7], coord[8], coord[0], coord[1], coord[2]);
            }
         }
      }

      triangles.callFuncs((lvl, pos) => {
         const geometry = createLegoGeom(this.getMainPainter(), pos, null, 100, 100),
               color = plain_mode ? this.getColor(graph.fFillColor) : palette.calcColor(lvl, levels.length),
               material = new MeshBasicMaterial(getMaterialArgs(color, { side: DoubleSide, vertexColors: false })),

          mesh = new Mesh(geometry, material);

         fp.add3DMesh(mesh, this);

         mesh.painter = this; // to let use it with context menu
      }, (_isgrid, lpos) => {
         const lcolor = this.getColor(graph.fLineColor),
              material = new LineBasicMaterial({ color: new Color(lcolor), linewidth: graph.fLineWidth }),
              linemesh = createLineSegments(convertLegoBuf(this.getMainPainter(), lpos, 100, 100), material);
         fp.add3DMesh(linemesh, this);
      });
   }

   /** @summary Update TGraph2D object */
   updateObject(obj, opt) {
      if (!this.matchObjectType(obj)) return false;

      if (opt && (opt !== this.options.original))
         this.decodeOptions(opt, obj);

      Object.assign(this.getObject(), obj);

      delete this._delaunay; // rebuild triangles

      delete this.$redraw_hist;

      // if our own histogram was used as axis drawing, we need update histogram as well
      if (this.axes_draw) {
         const hist_painter = this.getMainPainter();
         hist_painter?.updateObject(this.createHistogram(), this.options.Axis);
         this.$redraw_hist = hist_painter;
      }

      return true;
   }

   /** @summary Redraw TGraph2D object
     * @desc Update histogram drawing if necessary
     * @return {Promise} for drawing ready */
   async redraw() {
      let promise = Promise.resolve(true);

      if (this.$redraw_hist) {
         promise = this.$redraw_hist.redraw();
         delete this.$redraw_hist;
      }

      return promise.then(() => this.drawGraph2D());
   }

   /** @summary Actual drawing of TGraph2D object
     * @return {Promise} for drawing ready */
   async drawGraph2D() {
      const main = this.getMainPainter(),
            fp = this.getFramePainter(),
            graph = this.getObject();

      if (!graph || !main || !fp || !fp.mode3d)
         return this;

      fp.remove3DMeshes(this);

      if (!this.options.isAny()) {
         // no need to draw somthing if histogram content was drawn
         if (main.draw_content)
            return this;
         if ((graph.fMarkerSize === 1) && (graph.fMarkerStyle === 1))
            this.options.Circles = true;
         else
            this.options.Markers = true;
      }

      const countSelected = (zmin, zmax) => {
         let cnt = 0;
         for (let i = 0; i < graph.fNpoints; ++i) {
            if ((graph.fX[i] < fp.scale_xmin) || (graph.fX[i] > fp.scale_xmax) ||
                (graph.fY[i] < fp.scale_ymin) || (graph.fY[i] > fp.scale_ymax) ||
                (graph.fZ[i] < zmin) || (graph.fZ[i] >= zmax)) continue;

            ++cnt;
         }
         return cnt;
      };

      // try to define scale-down factor
      let step = 1;
      if ((settings.OptimizeDraw > 0) && !fp.webgl) {
         const numselected = countSelected(fp.scale_zmin, fp.scale_zmax),
             sizelimit = 50000;

         if (numselected > sizelimit) {
            step = Math.floor(numselected / sizelimit);
            if (step <= 2) step = 2;
         }
      }

      const markeratt = this.createAttMarker({ attr: graph, std: false }),
            promises = [];
      let palette = null,
          levels = [fp.scale_zmin, fp.scale_zmax],
          scale = fp.size_x3d / 100 * markeratt.getFullSize();

      if (this.options.Circles)
         scale = 0.06 * fp.size_x3d;

      if (fp.usesvg) scale *= 0.3;

      scale *= 7 * Math.max(fp.size_x3d / fp.getFrameWidth(), fp.size_z3d / fp.getFrameHeight());

      if (this.options.Color || (this.options.Triangles >= 10)) {
         levels = main.getContourLevels(true);
         palette = main.getHistPalette();
      }

      if (this.options.Triangles)
         this.drawTriangles(fp, graph, levels, palette);

      for (let lvl = 0; lvl < levels.length-1; ++lvl) {
         const lvl_zmin = Math.max(levels[lvl], fp.scale_zmin),
               lvl_zmax = Math.min(levels[lvl+1], fp.scale_zmax);

         if (lvl_zmin >= lvl_zmax) continue;

         const size = Math.floor(countSelected(lvl_zmin, lvl_zmax) / step),
               index = new Int32Array(size);
         let pnts = null, select = 0, icnt = 0,
             err = null, asymm = false, line = null, ierr = 0, iline = 0;

         if (this.options.Markers || this.options.Circles)
            pnts = new PointsCreator(size, fp.webgl, scale/3);

         if (this.options.Error) {
            err = new Float32Array(size*6*3);
            asymm = this.matchObjectType(clTGraph2DAsymmErrors);
          }

         if (this.options.Line)
            line = new Float32Array((size-1)*6);

         for (let i = 0; i < graph.fNpoints; ++i) {
            if ((graph.fX[i] < fp.scale_xmin) || (graph.fX[i] > fp.scale_xmax) ||
                (graph.fY[i] < fp.scale_ymin) || (graph.fY[i] > fp.scale_ymax) ||
                (graph.fZ[i] < lvl_zmin) || (graph.fZ[i] >= lvl_zmax)) continue;

            if (step > 1) {
               select = (select+1) % step;
               if (select !== 0) continue;
            }

            index[icnt++] = i; // remember point index for tooltip

            const x = fp.grx(graph.fX[i]),
                y = fp.gry(graph.fY[i]),
                z = fp.grz(graph.fZ[i]);

            if (pnts) pnts.addPoint(x, y, z);

            if (err) {
               err[ierr] = fp.grx(graph.fX[i] - (asymm ? graph.fEXlow[i] : graph.fEX[i]));
               err[ierr+1] = y;
               err[ierr+2] = z;
               err[ierr+3] = fp.grx(graph.fX[i] + (asymm ? graph.fEXhigh[i] : graph.fEX[i]));
               err[ierr+4] = y;
               err[ierr+5] = z;
               ierr+=6;
               err[ierr] = x;
               err[ierr+1] = fp.gry(graph.fY[i] - (asymm ? graph.fEYlow[i] : graph.fEY[i]));
               err[ierr+2] = z;
               err[ierr+3] = x;
               err[ierr+4] = fp.gry(graph.fY[i] + (asymm ? graph.fEYhigh[i] : graph.fEY[i]));
               err[ierr+5] = z;
               ierr+=6;
               err[ierr] = x;
               err[ierr+1] = y;
               err[ierr+2] = fp.grz(graph.fZ[i] - (asymm ? graph.fEZlow[i] : graph.fEZ[i]));
               err[ierr+3] = x;
               err[ierr+4] = y;
               err[ierr+5] = fp.grz(graph.fZ[i] + (asymm ? graph.fEZhigh[i] : graph.fEZ[i]));
               ierr+=6;
            }

            if (line) {
               if (iline>=6) {
                  line[iline] = line[iline-3];
                  line[iline+1] = line[iline-2];
                  line[iline+2] = line[iline-1];
                  iline+=3;
               }
               line[iline] = x;
               line[iline+1] = y;
               line[iline+2] = z;
               iline+=3;
            }
         }

         if (line && (iline > 3) && (line.length === iline)) {
            const lcolor = this.getColor(graph.fLineColor),
                  material = new LineBasicMaterial({ color: new Color(lcolor), linewidth: graph.fLineWidth }),
                  linemesh = createLineSegments(line, material);
            fp.add3DMesh(linemesh, this);

            linemesh.graph = graph;
            linemesh.index = index;
            linemesh.fp = fp;
            linemesh.scale0 = 0.7*scale;
            linemesh.tip_name = this.getObjectHint();
            linemesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
            linemesh.nvertex = 2;
            linemesh.check_next = true;

            linemesh.tooltip = graph2DTooltip;
         }

         if (err) {
            const lcolor = this.getColor(graph.fLineColor),
                  material = new LineBasicMaterial({ color: new Color(lcolor), linewidth: graph.fLineWidth }),
                  errmesh = createLineSegments(err, material);
            fp.add3DMesh(errmesh, this);

            errmesh.graph = graph;
            errmesh.index = index;
            errmesh.fp = fp;
            errmesh.scale0 = 0.7*scale;
            errmesh.tip_name = this.getObjectHint();
            errmesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
            errmesh.nvertex = 6;

            errmesh.tooltip = graph2DTooltip;
         }

         if (pnts) {
            let color = 'blue';

            if (!this.options.Circles || this.options.Color)
               color = palette?.calcColor(lvl, levels.length) ?? this.getColor(graph.fMarkerColor);

            const pr = pnts.createPoints({ color, style: this.options.Circles ? 4 : graph.fMarkerStyle }).then(mesh => {
               mesh.graph = graph;
               mesh.fp = fp;
               mesh.tip_color = (graph.fMarkerColor === 3) ? 0xFF0000 : 0x00FF00;
               mesh.scale0 = 0.3*scale;
               mesh.index = index;

               mesh.tip_name = this.getObjectHint();
               mesh.tooltip = graph2DTooltip;
               fp.add3DMesh(mesh, this);
            });

            promises.push(pr);
         }
      }

      return Promise.all(promises).then(() => {
         if (this.options.Zscale && this.axes_draw) {
            const pal = this.getMainPainter()?.findFunction(clTPaletteAxis),
                  pal_painter = this.getPadPainter()?.findPainterFor(pal);
            return pal_painter?.drawPave();
         }
      }).then(() => {
         fp.render3D(100);
         return this;
      });
   }

   /** @summary draw TGraph2D object */
   static async draw(dom, gr, opt) {
      const painter = new TGraph2DPainter(dom, gr);
      painter.decodeOptions(opt, gr);

      let promise = Promise.resolve(null);

      if (!painter.getMainPainter()) {
         // histogram is not preserved in TGraph2D
         promise = TH2Painter.draw(dom, painter.createHistogram(), painter.options.Axis);
         painter.axes_draw = true;
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         return painter.drawGraph2D();
      });
   }

} // class TGraph2DPainter

export { TGraph2DPainter };