<commit_before>/// @file JSRootGeoPainter.js
/// JavaScript ROOT 3D geometry painter

(function( factory ) {
   if ( typeof define === "function" && define.amd ) {
      // AMD. Register as an anonymous module.
      define( [ 'd3', 'JSRootPainter', 'JSRoot3DPainter', 'JSRootGeoBase' ], factory );
   } else {

      if (typeof JSROOT == 'undefined')
         throw new Error('JSROOT is not defined', 'JSRootGeoPainter.js');

      if (typeof JSROOT.Painter != 'object')
         throw new Error('JSROOT.Painter is not defined', 'JSRootGeoPainter.js');

      if (typeof d3 == 'undefined')
         throw new Error('d3 is not defined', 'JSRootGeoPainter.js');

      if (typeof THREE == 'undefined')
         throw new Error('THREE is not defined', 'JSRootGeoPainter.js');

      factory( d3, JSROOT);
   }
} (function( d3, JSROOT ) {

   if ( typeof define === "function" && define.amd )
      JSROOT.loadScript('$$$style/JSRootGeoPainter.css');

   if (typeof JSROOT.GEO !== 'object')
      console.error('JSROOT.GEO namespace is not defined')

   /**
    * @class JSROOT.TGeoPainter Holder of different functions and classes for drawing geometries
    */

   // ======= Geometry painter================================================


   JSROOT.TGeoPainter = function( geometry ) {
      if ((geometry !== null) && (geometry._typename.indexOf('TGeoVolume') === 0))
         geometry = { _typename:"TGeoNode", fVolume: geometry, fName:"TopLevel" };

      JSROOT.TObjectPainter.call(this, geometry);

      this.Cleanup(true);
   }

   JSROOT.TGeoPainter.prototype = Object.create( JSROOT.TObjectPainter.prototype );

   JSROOT.TGeoPainter.prototype.CreateToolbar = function(args) {
      if (this._toolbar) return;
      var painter = this;
      var buttonList = [{
         name: 'toImage',
         title: 'Save as PNG',
         icon: JSROOT.ToolbarIcons.camera,
         click: function() {
            var dataUrl = painter._renderer.domElement.toDataURL("image/png");
            dataUrl.replace("image/png", "image/octet-stream");
            var link = document.createElement('a');
            if (typeof link.download === 'string') {
               document.body.appendChild(link); //Firefox requires the link to be in the body
               link.download = "geometry.png";
               link.href = dataUrl;
               link.click();
               document.body.removeChild(link); //remove the link when done
            }
         }
      }];

      if (JSROOT.hpainter && JSROOT.hpainter.nobrowser)
         buttonList.push({
            name: 'browser',
            title: 'Show hierarchy browser',
            icon: JSROOT.ToolbarIcons.arrow_right,
            click: function() {
               if (JSROOT.hpainter)
                  JSROOT.hpainter.ToggleFloatBrowser();
            }
         });

      this._toolbar = new JSROOT.Toolbar( this.select_main(), [buttonList] );
   }

   JSROOT.TGeoPainter.prototype.ModifyVisisbility = function(name, sign) {
      var node = this.GetObject();

      var kind = JSROOT.GEO.NodeKind(node);
      var prop = JSROOT.GEO.getNodeProperties(kind, node);

      if (name == "")
         return JSROOT.GEO.SetBit(prop.volume, JSROOT.GEO.BITS.kVisThis, (sign === "+"));

      var regexp;

      if (name.indexOf("*") < 0)
         regexp = new RegExp(name);
      else
         regexp = new RegExp("^" + name.split("*").join(".*") + "$");

      if (prop.chlds!==null)
         for (var n=0;n<prop.chlds.length;++n) {
            var chld = JSROOT.GEO.getNodeProperties(kind, prop.chlds[n]);

            if (regexp.test(chld.name) && chld.volume) {
               JSROOT.GEO.SetBit(chld.volume, JSROOT.GEO.BITS.kVisThis, (sign === "+"));
               JSROOT.GEO.SetBit(chld.volume, JSROOT.GEO.BITS.kVisDaughters, (sign === "+"));
            }
         }
   }

   JSROOT.TGeoPainter.prototype.decodeOptions = function(opt) {
      var res = { _grid: false, _bound: false, _debug: false, _full: false, _axis:false, _count:false, scale: new THREE.Vector3(1,1,1), more:1, use_worker: false };

      var _opt = JSROOT.GetUrlOption('_grid');
      if (_opt !== null && _opt == "true") res._grid = true;
      var _opt = JSROOT.GetUrlOption('_debug');
      if (_opt !== null && _opt == "true") { res._debug = true; res._grid = true; }
      if (_opt !== null && _opt == "bound") { res._debug = true; res._grid = true; res._bound = true; }
      if (_opt !== null && _opt == "full") { res._debug = true; res._grid = true; res._full = true; res._bound = true; }

      while (true) {
         var pp = opt.indexOf("+"), pm = opt.indexOf("-");
         if ((pp<0) && (pm<0)) break;
         var p1 = pp, sign = "+";
         if ((p1<0) || ((pm>=0) && (pm<pp))) { p1 = pm; sign = "-"; }

         var p2 = p1+1, regexp = new RegExp('[,; .]');
         while ((p2<opt.length) && !regexp.test(opt[p2]) && (opt[p2]!='+') && (opt[p2]!='-')) p2++;

         var name = opt.substring(p1+1, p2);
         opt = opt.substr(0,p1) + opt.substr(p2);

         console.log("Modify visibility", sign,':',name);

         this.ModifyVisisbility(name, sign);
      }

      opt = opt.toLowerCase();

      if (opt.indexOf("more3")>=0) {
         opt = opt.replace("more3", " ");
         res.more = 3;
      }
      if (opt.indexOf("more")>=0) {
         opt = opt.replace("more", " ");
         res.more = 2;
      }
      if (opt.indexOf("all")>=0) {
         opt = opt.replace("all", " ");
         res.more = 100;
      }
      if (opt.indexOf("invx")>=0) {
         res.scale.x = -1;
         opt = opt.replace("invx", " ");
      }

      if (opt.indexOf("worker")>=0) {
         res.use_worker = true;
         opt = opt.replace("worker", " ");
      }

      if (opt.indexOf("invy")>=0) {
         res.scale.y = -1;
         opt = opt.replace("invy", " ");
      }
      if (opt.indexOf("invz")>=0) {
         res.scale.z = -1;
         opt = opt.replace("invz", " ");
      }

      if (opt.indexOf("count")>=0) {
         res._count = true;
         opt = opt.replace("count", " ");
      }

      if (opt.indexOf("d")>=0) res._debug = true;
      if (opt.indexOf("g")>=0) res._grid = true;
      if (opt.indexOf("b")>=0) res._bound = true;
      if (opt.indexOf("f")>=0) res._full = true;
      if (opt.indexOf("a")>=0) { res._axis = true; res._yup = false; }
      if (opt.indexOf("y")>=0) res._yup = true;
      if (opt.indexOf("z")>=0) res._yup = false;

      return res;
   }

   JSROOT.TGeoPainter.prototype.ActiavteInBrowser = function(names, force) {
      if (typeof names == 'string') names = [ names ];

      if (this.GetItemName().length > 0)
         for (var n=0;n<names.length;++n)
            names[n] = this.GetItemName() + '/' + names[n];

      if (JSROOT.hpainter)
         JSROOT.hpainter.actiavte(names, force);
   }


   JSROOT.TGeoPainter.prototype.addControls = function() {

      if (this._controls) return;

      var painter = this;

      this.select_main().property('flex_block_drag', true);

      this._controls = new THREE.OrbitControls(this._camera, this._renderer.domElement);
      this._controls.enableDamping = false;
      this._controls.dampingFactor = 0.25;
      this._controls.enableZoom = true;
      this._controls.target.copy(this._lookat);
      this._controls.update();

      var mouse_ctxt = { x:0, y: 0, on: false },
          raycaster = new THREE.Raycaster(),
          control_active = false;

      this._controls.addEventListener( 'change', function() {
          mouse_ctxt.on = false; // disable context menu if any changes where done by orbit control
          painter.Render3D(0);
      });

      function GetMousePos(evnt, mouse) {
         mouse.x = ('offsetX' in evnt) ? evnt.offsetX : evnt.layerX;
         mouse.y = ('offsetY' in evnt) ? evnt.offsetY : evnt.layerY;
         mouse.clientX = evnt.clientX;
         mouse.clientY = evnt.clientY;
      }

      function GetIntersects(mouse) {
         var pnt = {
            x: mouse.x / painter._renderer.domElement.width * 2 - 1,
            y: -mouse.y / painter._renderer.domElement.height * 2 + 1
         }

         raycaster.setFromCamera( pnt, painter._camera );
         return raycaster.intersectObjects(painter._scene.children, true);
      }

      this._controls.addEventListener( 'start', function() {
         control_active = true;
      });

      this._controls.addEventListener( 'end', function() {
         control_active = false;
         if (!mouse_ctxt.on) return;
         mouse_ctxt.on = false;
         var intersects = GetIntersects(mouse_ctxt);

         if (!intersects || (intersects.length==0)) return;

         JSROOT.Painter.createMenu(function(menu) {
            menu.painter = painter; // set as this in callbacks
            var many = (intersects.length > 1);

            if (many) menu.add("header: Nodes");

            for (var n=0;n<intersects.length;++n) {
               var obj = intersects[n].object;
               var name = painter._clones.GetNodeName(obj.stack);

               menu.add((many ? "sub:" : "header:") + name.substr(6), name, function(arg) { this.ActiavteInBrowser([arg], true); });

               menu.add("Browse", name, function(arg) { this.ActiavteInBrowser([arg], true); });

               menu.add("Focus", n, function(arg) { console.log('Focus '+arg); })

               if (many) menu.add("endsub:");
            }

            menu.show(mouse_ctxt);
         });

      });

      this._context_menu = function(evnt) {
         GetMousePos(evnt, mouse_ctxt);
         mouse_ctxt.on = true;
      }

      this._renderer.domElement.addEventListener( 'contextmenu', this._context_menu, false );

      if (this._webgl) {
         this._datgui = new dat.GUI({ width: Math.min(650, painter._renderer.domElement.width / 2) });
         var self = this;
         var toggleclip = this._datgui.add(this, 'enableClipping');
         toggleclip.onChange( function (value) {
            self.enableClipping = value;
            painter._scene.traverse(function(obj) {
               if (obj.hasOwnProperty("material") && ('emissive' in obj.material)) {
                  obj.material.side = painter.enableClipping ? THREE.DoubleSide : THREE.FrontSide;
                  obj.material.needsUpdate = true;
               }
            });
            self.updateClipping();
         });

         var xclip = this._datgui.add(this, 'clipX', -2000, 2000);
         var yclip = this._datgui.add(this, 'clipY', -2000, 2000);
         var zclip = this._datgui.add(this, 'clipZ', -2000, 2000);
         xclip.onChange( function (value) {
            self.clipX = value;
            self.updateClipping();
         });
         yclip.onChange( function (value) {
            self.clipY = value;
            self.updateClipping();
         });
         zclip.onChange( function (value) {
            self.clipZ = value;
            self.updateClipping();
         });
      }


      if ( this.options._debug || this.options._grid ) {
         this._tcontrols = new THREE.TransformControls( this._camera, this._renderer.domElement );
         this._scene.add( this._tcontrols );
         this._tcontrols.attach( this._toplevel );
         //this._tcontrols.setSize( 1.1 );

         window.addEventListener( 'keydown', function ( event ) {
            switch ( event.keyCode ) {
               case 81: // Q
                  painter._tcontrols.setSpace( painter._tcontrols.space === "local" ? "world" : "local" );
                  break;
               case 17: // Ctrl
                  painter._tcontrols.setTranslationSnap( Math.ceil( painter._overall_size ) / 50 );
                  painter._tcontrols.setRotationSnap( THREE.Math.degToRad( 15 ) );
                  break;
               case 84: // T (Translate)
                  painter._tcontrols.setMode( "translate" );
                  break;
               case 82: // R (Rotate)
                  painter._tcontrols.setMode( "rotate" );
                  break;
               case 83: // S (Scale)
                  painter._tcontrols.setMode( "scale" );
                  break;
               case 187:
               case 107: // +, =, num+
                  painter._tcontrols.setSize( painter._tcontrols.size + 0.1 );
                  break;
               case 189:
               case 109: // -, _, num-
                  painter._tcontrols.setSize( Math.max( painter._tcontrols.size - 0.1, 0.1 ) );
                  break;
            }
         });
         window.addEventListener( 'keyup', function ( event ) {
            switch ( event.keyCode ) {
               case 17: // Ctrl
                  painter._tcontrols.setTranslationSnap( null );
                  painter._tcontrols.setRotationSnap( null );
                  break;
            }
         });

         this._tcontrols.addEventListener( 'change', function() { painter.Render3D(0); } );
      }

      function mousemove(evnt) {
         if (control_active) return;

         var mouse = {};
         GetMousePos(evnt, mouse);
         evnt.preventDefault();

         var intersects = GetIntersects(mouse);
         var names = [];

         for (var n=0;n<intersects.length;++n) {
            var obj = intersects[n].object;
            var name = painter._clones.GetNodeName(obj.stack);
            names.push(name);
         }

         painter.ActiavteInBrowser(names);
      }

      this._renderer.domElement.addEventListener('mousemove', mousemove);

      function mouseleave(evnt) {
         painter.ActiavteInBrowser([]);
      }

      this._renderer.domElement.addEventListener('mouseleave', mouseleave);
   }

   JSROOT.TGeoPainter.prototype.accountClear = function() {
      this._num_geom = 0;
      this._num_vertices = 0;
      this._num_faces = 0;
      this._num_nodes = 0;
   }

   JSROOT.TGeoPainter.prototype.accountGeom = function(geom, shape_typename) {
      // used to calculate statistic over created geometries

      if (shape_typename === 'TGeoShapeAssembly') return;

      if (!geom) return JSROOT.GEO.warn('Not supported ' + shape_typename);

      this._num_geom++;

      this._num_faces += JSROOT.GEO.numGeometryFaces(geom);

      this._num_vertices += JSROOT.GEO.numGeometryVertices(geom);
   }

   JSROOT.TGeoPainter.prototype.accountNodes = function(mesh) {
      // used to calculate statistic over created meshes
      if (mesh !== null) this._num_nodes++;
   }

   JSROOT.TGeoPainter.prototype.createFlippedMesh = function(parent, shape, material) {
      // when transformation matrix includes one or several invertion of axis,
      // one should inverse geometry object, otherwise THREE.js cannot correctly draw it

      var m = parent.matrixWorld;

      var cnt = 0, flip = new THREE.Vector3(1,1,1);

      if (m.elements[0]===-1 && m.elements[1]=== 0 && m.elements[2] === 0) { flip.x = -1; cnt++; }
      if (m.elements[4]=== 0 && m.elements[5]===-1 && m.elements[6] === 0) { flip.y = -1; cnt++; }
      if (m.elements[8]=== 0 && m.elements[9]=== 0 && m.elements[10]===-1) { flip.z = -1; cnt++; }

      if ((cnt===0) || (cnt ===2)) {
         flip.set(1,1,1); cnt = 0;
         if (m.elements[0] + m.elements[1] + m.elements[2] === -1) { flip.x = -1; cnt++; }
         if (m.elements[4] + m.elements[5] + m.elements[6] === -1) { flip.y = -1; cnt++; }
         if (m.elements[8] + m.elements[9] + m.elements[10] === -1) { flip.z = -1; cnt++; }
         if ((cnt === 0) || (cnt === 2)) {
            // console.log('not found proper axis, use Z ' + JSON.stringify(flip) + '  m = ' + JSON.stringify(m.elements));
            flip.z = -flip.z;
         }
      }
      var gname = "_geom";
      if (flip.x<0) gname += "X";
      if (flip.y<0) gname += "Y";
      if (flip.z<0) gname += "Z";

      var geom = shape[gname];

      if (geom === undefined) {

         geom = shape._geom.clone();

         geom.scale(flip.x, flip.y, flip.z);

         if (geom.type == 'BufferGeometry') {

            var attr = geom.getAttribute('position'), d;

            // we should swap second and third point in each face
            for (var n=0;n<attr.array.length;n+=9)
               for (var k=0;k<3;++k) {
                  d = attr.array[n+k+3];
                  attr.array[n+k+3] = attr.array[n+k+6];
                  attr.array[n+k+6] = d;
               }

            // normals are calculated with normal geometry and correctly scaled
            // geom.computeVertexNormals();

         } else {

            var face, d;
            for (var n=0;n<geom.faces.length;++n) {
               face = geom.faces[n];
               d = face.b; face.b = face.c; face.c = d;
            }

            // normals are calculated with normal geometry and correctly scaled
            // geom.computeFaceNormals();
         }

         shape[gname] = geom;

         this.accountGeom(geom, shape._typename);
      }

      var mesh = new THREE.Mesh( geom, material );

      mesh.scale.copy(flip);

      mesh.updateMatrix();

      return mesh;
   }


   JSROOT.TGeoPainter.prototype.drawNode = function() {
      // return false when nothing todo
      // return true if creates next node
      // return 1 when call after short timeout
      // return 2 when call be done from processWorkerReply

      if (!this._clones || !this._draw_nodes || this._draw_nodes_ready) return false;

      // first of all, create geometries (using worker if available)

      var unique = [], todo = [], ready = [], waiting = 0;

      for (var n=0;n<this._draw_nodes.length;++n) {
         var entry = this._draw_nodes[n];
         if (entry.done) continue;

         var shape = this._clones.GetNodeShape(entry.nodeid);

         if (!shape) { entry.done = true; continue; }

         // if not geometry exists, either create it or submit to worker
         if (shape._geom !== undefined) {
            if (ready.length < 1000) ready.push(n);
         } else
         if (shape._geom_worker) {
            waiting++; // number of waiting for worker
         } else
         if (unique.indexOf(shape) < 0) {
            unique.push(shape); // only to avoid duplication
            todo.push({ indx: n, nodeid: entry.nodeid, shape: shape });
            if (todo.length > 50) break;
         }
      }

      // console.log('collected todo', todo.length,'ready', ready.length, 'waiting', waiting);

      if ((todo.length > 0) && this.options.use_worker) {
         if (this.canSubmitToWorker()) {
            for (var s=0;s<todo.length;++s) {
               unique[s]._geom_worker = true; // mark shape as processed by worker
               todo[s].shape = JSROOT.clone(todo[s].shape, null, true);
            }
            waiting += todo.length;
            this.submitToWorker({ shapes: todo });
         } else {
            if (!this._worker_ready) return 1;
         }

         todo = [];
      }

      // we wait reply from the worker, no need to set timeout
      if ((waiting > 0) && this.options.use_worker && (ready.length == 0)) return 2;

      // create geometries
      if (todo.length > 0) {
         for (var s=0;s<todo.length;++s) {
            var shape = todo[s].shape;
            shape._geom = JSROOT.GEO.createGeometry(shape);
            this.accountGeom(shape._geom, shape._typename);
            delete shape._geom_worker; // remove flag
            ready.push(todo[s].indx); // one could add it to ready list
         }
      }

      // create
      for (var n=0;n<ready.length;++n) {
         // item to draw, containes indexs of children, first element - node index

         var entry = this._draw_nodes[ready[n]];

         var obj3d = this._clones.CreateObject3D(entry.stack, this._toplevel, this.options);

         // original object, extracted from the map
         var nodeobj = this._clones.origin[entry.nodeid];
         var clone = this._clones.nodes[entry.nodeid];

         var prop = JSROOT.GEO.getNodeProperties(clone.kind, nodeobj, true);

         this._drawcnt++;

         var mesh = null;

         if (JSROOT.GEO.numGeometryFaces(prop.shape._geom) > 0) {
            if (obj3d.matrixWorld.determinant() > -0.9) {
               mesh = new THREE.Mesh( prop.shape._geom, prop.material );
            } else {
               mesh = this.createFlippedMesh(obj3d, prop.shape, prop.material);
            }

            // keep full stack of nodes
            mesh.stack = entry.stack;

            obj3d.add(mesh);
         }

         if (mesh && (this.options._debug || this.options._full)) {
            var helper = new THREE.WireframeHelper(mesh);
            helper.material.color.set(prop.fillcolor);
            helper.material.linewidth = ('fVolume' in nodeobj) ? nodeobj.fVolume.fLineWidth : 1;
            obj3d.add(helper);
         }

         if (mesh && (this.options._bound || this.options._full)) {
            var boxHelper = new THREE.BoxHelper( mesh );
            obj3d.add( boxHelper );
         }

         entry.done = true; // mark element as processed
      }

      // doing its job well, can be called next time
      if ((todo.length > 0) || (ready.length > 0)) return true;

      // worker does not deliver results, wait little longer
      if (waiting > 0) return 1;

      // here everything is completed, we could cleanup data and finish

      this._draw_nodes_ready = true;

      return false;
   }

   JSROOT.TGeoPainter.prototype.SameMaterial = function(node1, node2) {

      if ((node1===null) || (node2===null)) return node1 === node2;

      if (node1.fVolume.fLineColor >= 0)
         return (node1.fVolume.fLineColor === node2.fVolume.fLineColor);

       var m1 = (node1.fVolume.fMedium !== null) ? node1.fVolume.fMedium.fMaterial : null;
       var m2 = (node2.fVolume.fMedium !== null) ? node2.fVolume.fMedium.fMaterial : null;

       if (m1 === m2) return true;

       if ((m1 === null) || (m2 === null)) return false;

       return (m1.fFillStyle === m2.fFillStyle) && (m1.fFillColor === m2.fFillColor);
    }

   JSROOT.TGeoPainter.prototype.createScene = function(webgl, w, h, pixel_ratio) {
      // three.js 3D drawing
      this._scene = new THREE.Scene();
      this._scene.fog = new THREE.Fog(0xffffff, 500, 300000);
      this._scene.overrideMaterial = new THREE.MeshLambertMaterial( { color: 0x7000ff, transparent: true, opacity: 0.2, depthTest: false } );

      this._scene_width = w;
      this._scene_height = h;

      this._camera = new THREE.PerspectiveCamera(25, w / h, 1, 100000);

      this._renderer = webgl ?
                        new THREE.WebGLRenderer({ antialias : true, logarithmicDepthBuffer: true,
                                                  preserveDrawingBuffer: true }) :
                        new THREE.CanvasRenderer({antialias : true });
      this._renderer.setPixelRatio(pixel_ratio);
      this._renderer.setClearColor(0xffffff, 1);
      this._renderer.setSize(w, h);

      // Clipping Planes
      this.enableClipping = false;
      this.clipX = 0.0;
      this.clipY = 0.0;
      this.clipZ = 0.0;

      this._clipPlanes = [ new THREE.Plane(new THREE.Vector3( 1, 0, 0), this.clipX),
                           new THREE.Plane(new THREE.Vector3( 0,-1, 0), this.clipY),
                           new THREE.Plane(new THREE.Vector3( 0, 0, 1), this.clipZ) ];

      // TODO: should we change/increase number of light points??
      var pointLight = new THREE.PointLight(0xefefef);
      this._camera.add( pointLight );
      pointLight.position.set(10, 10, 10);
      this._camera.up = this.options._yup ? new THREE.Vector3(0,1,0) : new THREE.Vector3(0,0,1);
      this._scene.add( this._camera );

      this._toplevel = new THREE.Object3D();

      this._scene.add(this._toplevel);

      this._overall_size = 10;
   }


   JSROOT.TGeoPainter.prototype.startDrawGeometry = function(force) {

      if (!force && !this._draw_nodes_ready) {
         this._draw_nodes_again = true;
         return;
      }

      this.accountClear();

      tm1 = new Date().getTime();

      if (this._draw_nodes_again) this._clones.MarkVisisble();

      var res2 = this._clones.DefineVisible(this.options.maxlimit);
      var newnodes = this._clones.CollectVisibles(res2.minVol);
      tm2 = new Date().getTime();

      console.log('Collect visibles', newnodes.length, 'minvol', res2.minVol, 'takes', tm2-tm1);

      if (this._draw_nodes) {
         var del = this._clones.MergeVisibles(newnodes, this._draw_nodes);
         // remove should be fast, do it here
         for (var n=0;n<del.length;++n) {
            var obj3d = this._clones.CreateObject3D(del[n].stack, this._toplevel, this.options);
            if (obj3d) obj3d.parent.remove(obj3d);
         }
      }

      this._draw_nodes = newnodes;

      this._startm = new Date().getTime();
      this._last_render_tm = this._startm;
      this._last_render_cnt = 0;
      this._drawcnt = 0; // counter used to build meshes
      this._draw_nodes_ready = false;
      delete this._draw_nodes_again; // forget about such flag

      this.continueDraw();
   }

   JSROOT.TGeoPainter.prototype.updateClipping = function(offset) {
      this._renderer.clippingPlanes = this.enableClipping ? this._clipPlanes : [];
      this._clipPlanes[0].constant = this.clipX;
      this._clipPlanes[1].constant = this.clipY;
      this._clipPlanes[2].constant = this.clipZ;
      this.Render3D(0);
   }

   JSROOT.TGeoPainter.prototype.adjustCameraPosition = function() {

      var box = new THREE.Box3().setFromObject(this._toplevel);

      var sizex = box.max.x - box.min.x,
          sizey = box.max.y - box.min.y,
          sizez = box.max.z - box.min.z,
          midx = (box.max.x + box.min.x)/2,
          midy = (box.max.y + box.min.y)/2,
          midz = (box.max.z + box.min.z)/2;

      this._overall_size = 2 * Math.max( sizex, sizey, sizez);<commit_msg>Choose number of clipping planes<commit_after>/// @file JSRootGeoPainter.js
/// JavaScript ROOT 3D geometry painter

(function( factory ) {
   if ( typeof define === "function" && define.amd ) {
      // AMD. Register as an anonymous module.
      define( [ 'd3', 'JSRootPainter', 'JSRoot3DPainter', 'JSRootGeoBase' ], factory );
   } else {

      if (typeof JSROOT == 'undefined')
         throw new Error('JSROOT is not defined', 'JSRootGeoPainter.js');

      if (typeof JSROOT.Painter != 'object')
         throw new Error('JSROOT.Painter is not defined', 'JSRootGeoPainter.js');

      if (typeof d3 == 'undefined')
         throw new Error('d3 is not defined', 'JSRootGeoPainter.js');

      if (typeof THREE == 'undefined')
         throw new Error('THREE is not defined', 'JSRootGeoPainter.js');

      factory( d3, JSROOT);
   }
} (function( d3, JSROOT ) {

   if ( typeof define === "function" && define.amd )
      JSROOT.loadScript('$$$style/JSRootGeoPainter.css');

   if (typeof JSROOT.GEO !== 'object')
      console.error('JSROOT.GEO namespace is not defined')

   /**
    * @class JSROOT.TGeoPainter Holder of different functions and classes for drawing geometries
    */

   // ======= Geometry painter================================================


   JSROOT.TGeoPainter = function( geometry ) {
      if ((geometry !== null) && (geometry._typename.indexOf('TGeoVolume') === 0))
         geometry = { _typename:"TGeoNode", fVolume: geometry, fName:"TopLevel" };

      JSROOT.TObjectPainter.call(this, geometry);

      this.Cleanup(true);
   }

   JSROOT.TGeoPainter.prototype = Object.create( JSROOT.TObjectPainter.prototype );

   JSROOT.TGeoPainter.prototype.CreateToolbar = function(args) {
      if (this._toolbar) return;
      var painter = this;
      var buttonList = [{
         name: 'toImage',
         title: 'Save as PNG',
         icon: JSROOT.ToolbarIcons.camera,
         click: function() {
            var dataUrl = painter._renderer.domElement.toDataURL("image/png");
            dataUrl.replace("image/png", "image/octet-stream");
            var link = document.createElement('a');
            if (typeof link.download === 'string') {
               document.body.appendChild(link); //Firefox requires the link to be in the body
               link.download = "geometry.png";
               link.href = dataUrl;
               link.click();
               document.body.removeChild(link); //remove the link when done
            }
         }
      }];

      if (JSROOT.hpainter && JSROOT.hpainter.nobrowser)
         buttonList.push({
            name: 'browser',
            title: 'Show hierarchy browser',
            icon: JSROOT.ToolbarIcons.arrow_right,
            click: function() {
               if (JSROOT.hpainter)
                  JSROOT.hpainter.ToggleFloatBrowser();
            }
         });

      this._toolbar = new JSROOT.Toolbar( this.select_main(), [buttonList] );
   }

   JSROOT.TGeoPainter.prototype.ModifyVisisbility = function(name, sign) {
      var node = this.GetObject();

      var kind = JSROOT.GEO.NodeKind(node);
      var prop = JSROOT.GEO.getNodeProperties(kind, node);

      if (name == "")
         return JSROOT.GEO.SetBit(prop.volume, JSROOT.GEO.BITS.kVisThis, (sign === "+"));

      var regexp;

      if (name.indexOf("*") < 0)
         regexp = new RegExp(name);
      else
         regexp = new RegExp("^" + name.split("*").join(".*") + "$");

      if (prop.chlds!==null)
         for (var n=0;n<prop.chlds.length;++n) {
            var chld = JSROOT.GEO.getNodeProperties(kind, prop.chlds[n]);

            if (regexp.test(chld.name) && chld.volume) {
               JSROOT.GEO.SetBit(chld.volume, JSROOT.GEO.BITS.kVisThis, (sign === "+"));
               JSROOT.GEO.SetBit(chld.volume, JSROOT.GEO.BITS.kVisDaughters, (sign === "+"));
            }
         }
   }

   JSROOT.TGeoPainter.prototype.decodeOptions = function(opt) {
      var res = { _grid: false, _bound: false, _debug: false, _full: false, _axis:false, _count:false, scale: new THREE.Vector3(1,1,1), more:1, use_worker: false };

      var _opt = JSROOT.GetUrlOption('_grid');
      if (_opt !== null && _opt == "true") res._grid = true;
      var _opt = JSROOT.GetUrlOption('_debug');
      if (_opt !== null && _opt == "true") { res._debug = true; res._grid = true; }
      if (_opt !== null && _opt == "bound") { res._debug = true; res._grid = true; res._bound = true; }
      if (_opt !== null && _opt == "full") { res._debug = true; res._grid = true; res._full = true; res._bound = true; }

      while (true) {
         var pp = opt.indexOf("+"), pm = opt.indexOf("-");
         if ((pp<0) && (pm<0)) break;
         var p1 = pp, sign = "+";
         if ((p1<0) || ((pm>=0) && (pm<pp))) { p1 = pm; sign = "-"; }

         var p2 = p1+1, regexp = new RegExp('[,; .]');
         while ((p2<opt.length) && !regexp.test(opt[p2]) && (opt[p2]!='+') && (opt[p2]!='-')) p2++;

         var name = opt.substring(p1+1, p2);
         opt = opt.substr(0,p1) + opt.substr(p2);

         console.log("Modify visibility", sign,':',name);

         this.ModifyVisisbility(name, sign);
      }

      opt = opt.toLowerCase();

      if (opt.indexOf("more3")>=0) {
         opt = opt.replace("more3", " ");
         res.more = 3;
      }
      if (opt.indexOf("more")>=0) {
         opt = opt.replace("more", " ");
         res.more = 2;
      }
      if (opt.indexOf("all")>=0) {
         opt = opt.replace("all", " ");
         res.more = 100;
      }
      if (opt.indexOf("invx")>=0) {
         res.scale.x = -1;
         opt = opt.replace("invx", " ");
      }

      if (opt.indexOf("worker")>=0) {
         res.use_worker = true;
         opt = opt.replace("worker", " ");
      }

      if (opt.indexOf("invy")>=0) {
         res.scale.y = -1;
         opt = opt.replace("invy", " ");
      }
      if (opt.indexOf("invz")>=0) {
         res.scale.z = -1;
         opt = opt.replace("invz", " ");
      }

      if (opt.indexOf("count")>=0) {
         res._count = true;
         opt = opt.replace("count", " ");
      }

      if (opt.indexOf("d")>=0) res._debug = true;
      if (opt.indexOf("g")>=0) res._grid = true;
      if (opt.indexOf("b")>=0) res._bound = true;
      if (opt.indexOf("f")>=0) res._full = true;
      if (opt.indexOf("a")>=0) { res._axis = true; res._yup = false; }
      if (opt.indexOf("y")>=0) res._yup = true;
      if (opt.indexOf("z")>=0) res._yup = false;

      return res;
   }

   JSROOT.TGeoPainter.prototype.ActiavteInBrowser = function(names, force) {
      if (typeof names == 'string') names = [ names ];

      if (this.GetItemName().length > 0)
         for (var n=0;n<names.length;++n)
            names[n] = this.GetItemName() + '/' + names[n];

      if (JSROOT.hpainter)
         JSROOT.hpainter.actiavte(names, force);
   }


   JSROOT.TGeoPainter.prototype.addControls = function() {

      if (this._controls) return;

      var painter = this;

      this.select_main().property('flex_block_drag', true);

      this._controls = new THREE.OrbitControls(this._camera, this._renderer.domElement);
      this._controls.enableDamping = false;
      this._controls.dampingFactor = 0.25;
      this._controls.enableZoom = true;
      this._controls.target.copy(this._lookat);
      this._controls.update();

      var mouse_ctxt = { x:0, y: 0, on: false },
          raycaster = new THREE.Raycaster(),
          control_active = false;

      this._controls.addEventListener( 'change', function() {
          mouse_ctxt.on = false; // disable context menu if any changes where done by orbit control
          painter.Render3D(0);
      });

      function GetMousePos(evnt, mouse) {
         mouse.x = ('offsetX' in evnt) ? evnt.offsetX : evnt.layerX;
         mouse.y = ('offsetY' in evnt) ? evnt.offsetY : evnt.layerY;
         mouse.clientX = evnt.clientX;
         mouse.clientY = evnt.clientY;
      }

      function GetIntersects(mouse) {
         var pnt = {
            x: mouse.x / painter._renderer.domElement.width * 2 - 1,
            y: -mouse.y / painter._renderer.domElement.height * 2 + 1
         }

         raycaster.setFromCamera( pnt, painter._camera );
         return raycaster.intersectObjects(painter._scene.children, true);
      }

      this._controls.addEventListener( 'start', function() {
         control_active = true;
      });

      this._controls.addEventListener( 'end', function() {
         control_active = false;
         if (!mouse_ctxt.on) return;
         mouse_ctxt.on = false;
         var intersects = GetIntersects(mouse_ctxt);

         if (!intersects || (intersects.length==0)) return;

         JSROOT.Painter.createMenu(function(menu) {
            menu.painter = painter; // set as this in callbacks
            var many = (intersects.length > 1);

            if (many) menu.add("header: Nodes");

            for (var n=0;n<intersects.length;++n) {
               var obj = intersects[n].object;
               var name = painter._clones.GetNodeName(obj.stack);

               menu.add((many ? "sub:" : "header:") + name.substr(6), name, function(arg) { this.ActiavteInBrowser([arg], true); });

               menu.add("Browse", name, function(arg) { this.ActiavteInBrowser([arg], true); });

               menu.add("Focus", n, function(arg) { console.log('Focus '+arg); })

               if (many) menu.add("endsub:");
            }

            menu.show(mouse_ctxt);
         });

      });

      this._context_menu = function(evnt) {
         GetMousePos(evnt, mouse_ctxt);
         mouse_ctxt.on = true;
      }

      this._renderer.domElement.addEventListener( 'contextmenu', this._context_menu, false );

      if (this._webgl) {
         this._datgui = new dat.GUI({ width: Math.min(650, painter._renderer.domElement.width / 2) });
         var toggleclip = this._datgui.add(this, 'activePlanes', [0, 1, 2, 3]);
         toggleclip.onChange( function (value) {
            painter.activePlanes = value;
            painter._scene.traverse(function(obj) {
               if (obj.hasOwnProperty("material") && ('emissive' in obj.material)) {
                  obj.material.side = (painter.activePlanes > 0) ? THREE.DoubleSide : THREE.FrontSide;
                  obj.material.needsUpdate = true;
               }
            });
            painter.updateClipping();
         });

         var xclip = this._datgui.add(this, 'clipX', -2000, 2000);
         var yclip = this._datgui.add(this, 'clipY', -2000, 2000);
         var zclip = this._datgui.add(this, 'clipZ', -2000, 2000);
         xclip.onChange( function (value) {
            painter.clipX = value;
            painter.updateClipping();
         });
         yclip.onChange( function (value) {
            painter.clipY = value;
            painter.updateClipping();
         });
         zclip.onChange( function (value) {
            painter.clipZ = value;
            painter.updateClipping();
         });
      }


      if ( this.options._debug || this.options._grid ) {
         this._tcontrols = new THREE.TransformControls( this._camera, this._renderer.domElement );
         this._scene.add( this._tcontrols );
         this._tcontrols.attach( this._toplevel );
         //this._tcontrols.setSize( 1.1 );

         window.addEventListener( 'keydown', function ( event ) {
            switch ( event.keyCode ) {
               case 81: // Q
                  painter._tcontrols.setSpace( painter._tcontrols.space === "local" ? "world" : "local" );
                  break;
               case 17: // Ctrl
                  painter._tcontrols.setTranslationSnap( Math.ceil( painter._overall_size ) / 50 );
                  painter._tcontrols.setRotationSnap( THREE.Math.degToRad( 15 ) );
                  break;
               case 84: // T (Translate)
                  painter._tcontrols.setMode( "translate" );
                  break;
               case 82: // R (Rotate)
                  painter._tcontrols.setMode( "rotate" );
                  break;
               case 83: // S (Scale)
                  painter._tcontrols.setMode( "scale" );
                  break;
               case 187:
               case 107: // +, =, num+
                  painter._tcontrols.setSize( painter._tcontrols.size + 0.1 );
                  break;
               case 189:
               case 109: // -, _, num-
                  painter._tcontrols.setSize( Math.max( painter._tcontrols.size - 0.1, 0.1 ) );
                  break;
            }
         });
         window.addEventListener( 'keyup', function ( event ) {
            switch ( event.keyCode ) {
               case 17: // Ctrl
                  painter._tcontrols.setTranslationSnap( null );
                  painter._tcontrols.setRotationSnap( null );
                  break;
            }
         });

         this._tcontrols.addEventListener( 'change', function() { painter.Render3D(0); } );
      }

      function mousemove(evnt) {
         if (control_active) return;

         var mouse = {};
         GetMousePos(evnt, mouse);
         evnt.preventDefault();

         var intersects = GetIntersects(mouse);
         var names = [];

         for (var n=0;n<intersects.length;++n) {
            var obj = intersects[n].object;
            var name = painter._clones.GetNodeName(obj.stack);
            names.push(name);
         }

         painter.ActiavteInBrowser(names);
      }

      this._renderer.domElement.addEventListener('mousemove', mousemove);

      function mouseleave(evnt) {
         painter.ActiavteInBrowser([]);
      }

      this._renderer.domElement.addEventListener('mouseleave', mouseleave);
   }

   JSROOT.TGeoPainter.prototype.accountClear = function() {
      this._num_geom = 0;
      this._num_vertices = 0;
      this._num_faces = 0;
      this._num_nodes = 0;
   }

   JSROOT.TGeoPainter.prototype.accountGeom = function(geom, shape_typename) {
      // used to calculate statistic over created geometries

      if (shape_typename === 'TGeoShapeAssembly') return;

      if (!geom) return JSROOT.GEO.warn('Not supported ' + shape_typename);

      this._num_geom++;

      this._num_faces += JSROOT.GEO.numGeometryFaces(geom);

      this._num_vertices += JSROOT.GEO.numGeometryVertices(geom);
   }

   JSROOT.TGeoPainter.prototype.accountNodes = function(mesh) {
      // used to calculate statistic over created meshes
      if (mesh !== null) this._num_nodes++;
   }

   JSROOT.TGeoPainter.prototype.createFlippedMesh = function(parent, shape, material) {
      // when transformation matrix includes one or several invertion of axis,
      // one should inverse geometry object, otherwise THREE.js cannot correctly draw it

      var m = parent.matrixWorld;

      var cnt = 0, flip = new THREE.Vector3(1,1,1);

      if (m.elements[0]===-1 && m.elements[1]=== 0 && m.elements[2] === 0) { flip.x = -1; cnt++; }
      if (m.elements[4]=== 0 && m.elements[5]===-1 && m.elements[6] === 0) { flip.y = -1; cnt++; }
      if (m.elements[8]=== 0 && m.elements[9]=== 0 && m.elements[10]===-1) { flip.z = -1; cnt++; }

      if ((cnt===0) || (cnt ===2)) {
         flip.set(1,1,1); cnt = 0;
         if (m.elements[0] + m.elements[1] + m.elements[2] === -1) { flip.x = -1; cnt++; }
         if (m.elements[4] + m.elements[5] + m.elements[6] === -1) { flip.y = -1; cnt++; }
         if (m.elements[8] + m.elements[9] + m.elements[10] === -1) { flip.z = -1; cnt++; }
         if ((cnt === 0) || (cnt === 2)) {
            // console.log('not found proper axis, use Z ' + JSON.stringify(flip) + '  m = ' + JSON.stringify(m.elements));
            flip.z = -flip.z;
         }
      }
      var gname = "_geom";
      if (flip.x<0) gname += "X";
      if (flip.y<0) gname += "Y";
      if (flip.z<0) gname += "Z";

      var geom = shape[gname];

      if (geom === undefined) {

         geom = shape._geom.clone();

         geom.scale(flip.x, flip.y, flip.z);

         if (geom.type == 'BufferGeometry') {

            var attr = geom.getAttribute('position'), d;

            // we should swap second and third point in each face
            for (var n=0;n<attr.array.length;n+=9)
               for (var k=0;k<3;++k) {
                  d = attr.array[n+k+3];
                  attr.array[n+k+3] = attr.array[n+k+6];
                  attr.array[n+k+6] = d;
               }

            // normals are calculated with normal geometry and correctly scaled
            // geom.computeVertexNormals();

         } else {

            var face, d;
            for (var n=0;n<geom.faces.length;++n) {
               face = geom.faces[n];
               d = face.b; face.b = face.c; face.c = d;
            }

            // normals are calculated with normal geometry and correctly scaled
            // geom.computeFaceNormals();
         }

         shape[gname] = geom;

         this.accountGeom(geom, shape._typename);
      }

      var mesh = new THREE.Mesh( geom, material );

      mesh.scale.copy(flip);

      mesh.updateMatrix();

      return mesh;
   }


   JSROOT.TGeoPainter.prototype.drawNode = function() {
      // return false when nothing todo
      // return true if creates next node
      // return 1 when call after short timeout
      // return 2 when call be done from processWorkerReply

      if (!this._clones || !this._draw_nodes || this._draw_nodes_ready) return false;

      // first of all, create geometries (using worker if available)

      var unique = [], todo = [], ready = [], waiting = 0;

      for (var n=0;n<this._draw_nodes.length;++n) {
         var entry = this._draw_nodes[n];
         if (entry.done) continue;

         var shape = this._clones.GetNodeShape(entry.nodeid);

         if (!shape) { entry.done = true; continue; }

         // if not geometry exists, either create it or submit to worker
         if (shape._geom !== undefined) {
            if (ready.length < 1000) ready.push(n);
         } else
         if (shape._geom_worker) {
            waiting++; // number of waiting for worker
         } else
         if (unique.indexOf(shape) < 0) {
            unique.push(shape); // only to avoid duplication
            todo.push({ indx: n, nodeid: entry.nodeid, shape: shape });
            if (todo.length > 50) break;
         }
      }

      // console.log('collected todo', todo.length,'ready', ready.length, 'waiting', waiting);

      if ((todo.length > 0) && this.options.use_worker) {
         if (this.canSubmitToWorker()) {
            for (var s=0;s<todo.length;++s) {
               unique[s]._geom_worker = true; // mark shape as processed by worker
               todo[s].shape = JSROOT.clone(todo[s].shape, null, true);
            }
            waiting += todo.length;
            this.submitToWorker({ shapes: todo });
         } else {
            if (!this._worker_ready) return 1;
         }

         todo = [];
      }

      // we wait reply from the worker, no need to set timeout
      if ((waiting > 0) && this.options.use_worker && (ready.length == 0)) return 2;

      // create geometries
      if (todo.length > 0) {
         for (var s=0;s<todo.length;++s) {
            var shape = todo[s].shape;
            shape._geom = JSROOT.GEO.createGeometry(shape);
            this.accountGeom(shape._geom, shape._typename);
            delete shape._geom_worker; // remove flag
            ready.push(todo[s].indx); // one could add it to ready list
         }
      }

      // create
      for (var n=0;n<ready.length;++n) {
         // item to draw, containes indexs of children, first element - node index

         var entry = this._draw_nodes[ready[n]];

         var obj3d = this._clones.CreateObject3D(entry.stack, this._toplevel, this.options);

         // original object, extracted from the map
         var nodeobj = this._clones.origin[entry.nodeid];
         var clone = this._clones.nodes[entry.nodeid];

         var prop = JSROOT.GEO.getNodeProperties(clone.kind, nodeobj, true);

         this._drawcnt++;

         var mesh = null;

         if (JSROOT.GEO.numGeometryFaces(prop.shape._geom) > 0) {
            if (obj3d.matrixWorld.determinant() > -0.9) {
               mesh = new THREE.Mesh( prop.shape._geom, prop.material );
            } else {
               mesh = this.createFlippedMesh(obj3d, prop.shape, prop.material);
            }

            // keep full stack of nodes
            mesh.stack = entry.stack;

            obj3d.add(mesh);
         }

         if (mesh && (this.options._debug || this.options._full)) {
            var helper = new THREE.WireframeHelper(mesh);
            helper.material.color.set(prop.fillcolor);
            helper.material.linewidth = ('fVolume' in nodeobj) ? nodeobj.fVolume.fLineWidth : 1;
            obj3d.add(helper);
         }

         if (mesh && (this.options._bound || this.options._full)) {
            var boxHelper = new THREE.BoxHelper( mesh );
            obj3d.add( boxHelper );
         }

         entry.done = true; // mark element as processed
      }

      // doing its job well, can be called next time
      if ((todo.length > 0) || (ready.length > 0)) return true;

      // worker does not deliver results, wait little longer
      if (waiting > 0) return 1;

      // here everything is completed, we could cleanup data and finish

      this._draw_nodes_ready = true;

      return false;
   }

   JSROOT.TGeoPainter.prototype.SameMaterial = function(node1, node2) {

      if ((node1===null) || (node2===null)) return node1 === node2;

      if (node1.fVolume.fLineColor >= 0)
         return (node1.fVolume.fLineColor === node2.fVolume.fLineColor);

       var m1 = (node1.fVolume.fMedium !== null) ? node1.fVolume.fMedium.fMaterial : null;
       var m2 = (node2.fVolume.fMedium !== null) ? node2.fVolume.fMedium.fMaterial : null;

       if (m1 === m2) return true;

       if ((m1 === null) || (m2 === null)) return false;

       return (m1.fFillStyle === m2.fFillStyle) && (m1.fFillColor === m2.fFillColor);
    }

   JSROOT.TGeoPainter.prototype.createScene = function(webgl, w, h, pixel_ratio) {
      // three.js 3D drawing
      this._scene = new THREE.Scene();
      this._scene.fog = new THREE.Fog(0xffffff, 500, 300000);
      this._scene.overrideMaterial = new THREE.MeshLambertMaterial( { color: 0x7000ff, transparent: true, opacity: 0.2, depthTest: false } );

      this._scene_width = w;
      this._scene_height = h;

      this._camera = new THREE.PerspectiveCamera(25, w / h, 1, 100000);

      this._renderer = webgl ?
                        new THREE.WebGLRenderer({ antialias : true, logarithmicDepthBuffer: true,
                                                  preserveDrawingBuffer: true }) :
                        new THREE.CanvasRenderer({antialias : true });
      this._renderer.setPixelRatio(pixel_ratio);
      this._renderer.setClearColor(0xffffff, 1);
      this._renderer.setSize(w, h);

      // Clipping Planes
    //  this.enableClipping = false;
      this.activePlanes = 0;
      this.clipX = 0.0;
      this.clipY = 0.0;
      this.clipZ = 0.0;

      this._clipPlanes = [ new THREE.Plane(new THREE.Vector3( 1, 0, 0), this.clipX),
                           new THREE.Plane(new THREE.Vector3( 0,-1, 0), this.clipY),
                           new THREE.Plane(new THREE.Vector3( 0, 0, 1), this.clipZ) ];

      // TODO: should we change/increase number of light points??
      var pointLight = new THREE.PointLight(0xefefef);
      this._camera.add( pointLight );
      pointLight.position.set(10, 10, 10);
      this._camera.up = this.options._yup ? new THREE.Vector3(0,1,0) : new THREE.Vector3(0,0,1);
      this._scene.add( this._camera );

      this._toplevel = new THREE.Object3D();

      this._scene.add(this._toplevel);

      this._overall_size = 10;
   }


   JSROOT.TGeoPainter.prototype.startDrawGeometry = function(force) {

      if (!force && !this._draw_nodes_ready) {
         this._draw_nodes_again = true;
         return;
      }

      this.accountClear();

      tm1 = new Date().getTime();

      if (this._draw_nodes_again) this._clones.MarkVisisble();

      var res2 = this._clones.DefineVisible(this.options.maxlimit);
      var newnodes = this._clones.CollectVisibles(res2.minVol);
      tm2 = new Date().getTime();

      console.log('Collect visibles', newnodes.length, 'minvol', res2.minVol, 'takes', tm2-tm1);

      if (this._draw_nodes) {
         var del = this._clones.MergeVisibles(newnodes, this._draw_nodes);
         // remove should be fast, do it here
         for (var n=0;n<del.length;++n) {
            var obj3d = this._clones.CreateObject3D(del[n].stack, this._toplevel, this.options);
            if (obj3d) obj3d.parent.remove(obj3d);
         }
      }

      this._draw_nodes = newnodes;

      this._startm = new Date().getTime();
      this._last_render_tm = this._startm;
      this._last_render_cnt = 0;
      this._drawcnt = 0; // counter used to build meshes
      this._draw_nodes_ready = false;
      delete this._draw_nodes_again; // forget about such flag

      this.continueDraw();
   }

   JSROOT.TGeoPainter.prototype.updateClipping = function(offset) {
      this._clipPlanes[0].constant = this.clipX;
      this._clipPlanes[1].constant = this.clipY;
      this._clipPlanes[2].constant = this.clipZ;
      this._renderer.clippingPlanes = [];
      for (var p = 0; p < this.activePlanes; ++p) {
         this._renderer.clippingPlanes.push(this._clipPlanes[p]);
      }
      this.Render3D(0);
   }

   JSROOT.TGeoPainter.prototype.adjustCameraPosition = function() {

      var box = new THREE.Box3().setFromObject(this._toplevel);

      var sizex = box.max.x - box.min.x,
          sizey = box.max.y - box.min.y,
          sizez = box.max.z - box.min.z,
          midx = (box.max.x + box.min.x)/2,
          midy = (box.max.y + box.min.y)/2,
          midz = (box.max.z + box.min.z)/2;

      this._overall_size = 2 * Math.max( sizex, sizey, sizez);
{'fraction_non_alphanumeric': 0.14692392545429914, 'fraction_numerical': 0.01410794805797999, 'mean_word_length': 2.0417942076760065, 'pattern_counts': {'":': 0, '<': 50, '<?xml version=': 0, '>': 60, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2332333', 'n_tokens_mistral': 16852, 'n_tokens_neox': 16244, 'n_words': 3461}