geom/TGeoPainter.mjs

import { httpRequest, browser, source_dir, settings, internals, constants, create, clone,
         findFunction, isBatchMode, isNodeJs, getDocument, isObject, isFunc, isStr, postponePromise, getPromise,
         prROOT, clTNamed, clTList, clTAxis, clTObjArray, clTPolyMarker3D, clTPolyLine3D,
         clTGeoVolume, clTGeoNode, clTGeoNodeMatrix, nsREX, kInspect } from '../core.mjs';
import { REVISION, DoubleSide, FrontSide,
         Color, Vector2, Vector3, Matrix4, Object3D, Box3, Group, Plane, PlaneHelper,
         Euler, Quaternion, Mesh, InstancedMesh, MeshLambertMaterial, MeshBasicMaterial,
         LineSegments, LineBasicMaterial, LineDashedMaterial, BufferAttribute,
         TextGeometry, BufferGeometry, BoxGeometry, CircleGeometry, SphereGeometry,
         Scene, Fog, OrthographicCamera, PerspectiveCamera,
         DirectionalLight, AmbientLight, HemisphereLight,
         EffectComposer, RenderPass, UnrealBloomPass } from '../three.mjs';
import { showProgress, injectStyle, ToolbarIcons } from '../gui/utils.mjs';
import { GUI } from '../gui/lil-gui.mjs';
import { assign3DHandler, disposeThreejsObject, createOrbitControl,
         createLineSegments, InteractiveControl, PointsCreator,
         createRender3D, beforeRender3D, afterRender3D, getRender3DKind, cleanupRender3D,
         HelveticerRegularFont } from '../base/base3d.mjs';
import { getColor, getRootColors } from '../base/colors.mjs';
import { DrawOptions } from '../base/BasePainter.mjs';
import { ObjectPainter } from '../base/ObjectPainter.mjs';
import { createMenu, closeMenu } from '../gui/menu.mjs';
import { TAxisPainter } from '../gpad/TAxisPainter.mjs';
import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';
import { kindGeo, kindEve,
         clTGeoBBox, clTGeoCompositeShape,
         geoCfg, geoBITS, ClonedNodes, testGeoBit, setGeoBit, toggleGeoBit, setInvisibleAll,
         countNumShapes, getNodeKind, produceRenderOrder, createServerGeometry,
         projectGeometry, countGeometryFaces, createMaterial, createFrustum, createProjectionMatrix,
         getBoundingBox, provideObjectInfo, isSameStack, checkDuplicates, getObjectName, cleanupShape, getShapeIcon } from './geobase.mjs';


const _ENTIRE_SCENE = 0, _BLOOM_SCENE = 1,
      clTGeoManager = 'TGeoManager', clTEveGeoShapeExtract = 'TEveGeoShapeExtract',
      clTGeoOverlap = 'TGeoOverlap', clTGeoVolumeAssembly = 'TGeoVolumeAssembly',
      clTEveTrack = 'TEveTrack', clTEvePointSet = 'TEvePointSet',
      clREveGeoShapeExtract = `${nsREX}REveGeoShapeExtract`;

/** @summary Function used to build hierarchy of elements of overlap object
  * @private */
function buildOverlapVolume(overlap) {
   const vol = create(clTGeoVolume);

   setGeoBit(vol, geoBITS.kVisDaughters, true);
   vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
   vol.fName = '';

   const node1 = create(clTGeoNodeMatrix);
   node1.fName = overlap.fVolume1.fName || 'Overlap1';
   node1.fMatrix = overlap.fMatrix1;
   node1.fVolume = overlap.fVolume1;
   // node1.fVolume.fLineColor = 2; // color assigned with _splitColors

   const node2 = create(clTGeoNodeMatrix);
   node2.fName = overlap.fVolume2.fName || 'Overlap2';
   node2.fMatrix = overlap.fMatrix2;
   node2.fVolume = overlap.fVolume2;
   // node2.fVolume.fLineColor = 3;  // color assigned with _splitColors

   vol.fNodes = create(clTList);
   vol.fNodes.Add(node1);
   vol.fNodes.Add(node2);

   return vol;
}

let $comp_col_cnt = 0;

/** @summary Function used to build hierarchy of elements of composite shapes
  * @private */
function buildCompositeVolume(comp, maxlvl, side) {
   if (maxlvl === undefined) maxlvl = 1;
   if (!side) {
      $comp_col_cnt = 0;
      side = '';
   }

   const vol = create(clTGeoVolume);
   setGeoBit(vol, geoBITS.kVisThis, true);
   setGeoBit(vol, geoBITS.kVisDaughters, true);

   if ((side && (comp._typename !== clTGeoCompositeShape)) || (maxlvl <= 0)) {
      vol.fName = side;
      vol.fLineColor = ($comp_col_cnt++ % 8) + 2;
      vol.fShape = comp;
      return vol;
   }

   if (side) side += '/';
   vol.$geoh = true; // workaround, let know browser that we are in volumes hierarchy
   vol.fName = '';

   const node1 = create(clTGeoNodeMatrix);
   setGeoBit(node1, geoBITS.kVisThis, true);
   setGeoBit(node1, geoBITS.kVisDaughters, true);
   node1.fName = 'Left';
   node1.fMatrix = comp.fNode.fLeftMat;
   node1.fVolume = buildCompositeVolume(comp.fNode.fLeft, maxlvl-1, side + 'Left');

   const node2 = create(clTGeoNodeMatrix);
   setGeoBit(node2, geoBITS.kVisThis, true);
   setGeoBit(node2, geoBITS.kVisDaughters, true);
   node2.fName = 'Right';
   node2.fMatrix = comp.fNode.fRightMat;
   node2.fVolume = buildCompositeVolume(comp.fNode.fRight, maxlvl-1, side + 'Right');

   vol.fNodes = create(clTList);
   vol.fNodes.Add(node1);
   vol.fNodes.Add(node2);

   if (!side) $comp_col_cnt = 0;

   return vol;
}


/** @summary Provides 3D rendering configuration from histogram painter
  * @return {Object} with scene, renderer and other attributes
  * @private */
function getHistPainter3DCfg(painter) {
   const main = painter?.getFramePainter();
   if (painter?.mode3d && isFunc(main?.create3DScene) && main?.renderer) {
      let scale_x = 1, scale_y = 1, scale_z = 1,
          offset_x = 0, offset_y = 0, offset_z = 0;

      if (main.scale_xmax > main.scale_xmin) {
         scale_x = 2 * main.size_x3d/(main.scale_xmax - main.scale_xmin);
         offset_x = (main.scale_xmax + main.scale_xmin) / 2 * scale_x;
      }

      if (main.scale_ymax > main.scale_ymin) {
         scale_y = 2 * main.size_y3d/(main.scale_ymax - main.scale_ymin);
         offset_y = (main.scale_ymax + main.scale_ymin) / 2 * scale_y;
      }

      if (main.scale_zmax > main.scale_zmin) {
         scale_z = 2 * main.size_z3d/(main.scale_zmax - main.scale_zmin);
         offset_z = (main.scale_zmax + main.scale_zmin) / 2 * scale_z - main.size_z3d;
      }

      return {
         webgl: main.webgl,
         scene: main.scene,
         scene_width: main.scene_width,
         scene_height: main.scene_height,
         toplevel: main.toplevel,
         renderer: main.renderer,
         camera: main.camera,
         scale_x, scale_y, scale_z,
         offset_x, offset_y, offset_z
      };
  }
}


/** @summary create list entity for geo object
  * @private */
function createList(parent, lst, name, title) {
   if (!lst?.arr?.length) return;

   const list_item = {
       _name: name,
       _kind: prROOT + clTList,
       _title: title,
       _more: true,
       _geoobj: lst,
       _parent: parent,
       _get(item /*, itemname */) {
          return Promise.resolve(item._geoobj || null);
       },
       _expand(node, lst) {
          // only childs

          if (lst.fVolume)
             lst = lst.fVolume.fNodes;

          if (!lst.arr) return false;

          node._childs = [];

          checkDuplicates(null, lst.arr);

          for (const n in lst.arr)
             createItem(node, lst.arr[n]);

          return true;
       }
   };

   if (!parent._childs)
      parent._childs = [];
   parent._childs.push(list_item);
}


/** @summary Expand geo object
  * @private */
function expandGeoObject(parent, obj) {
   injectGeoStyle();

   if (!parent || !obj) return false;

   const isnode = (obj._typename.indexOf(clTGeoNode) === 0),
         isvolume = (obj._typename.indexOf(clTGeoVolume) === 0),
         ismanager = (obj._typename === clTGeoManager),
         iseve = ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)),
         isoverlap = (obj._typename === clTGeoOverlap);

   if (!isnode && !isvolume && !ismanager && !iseve && !isoverlap) return false;

   if (parent._childs) return true;

   if (ismanager) {
      createList(parent, obj.fMaterials, 'Materials', 'list of materials');
      createList(parent, obj.fMedia, 'Media', 'list of media');
      createList(parent, obj.fTracks, 'Tracks', 'list of tracks');
      createList(parent, obj.fOverlaps, 'Overlaps', 'list of detected overlaps');
      createItem(parent, obj.fMasterVolume);
      return true;
   }

   if (isoverlap) {
      createItem(parent, obj.fVolume1);
      createItem(parent, obj.fVolume2);
      createItem(parent, obj.fMarker, 'Marker');
      return true;
   }

   let volume, subnodes, shape;

   if (iseve) {
      subnodes = obj.fElements?.arr;
      shape = obj.fShape;
   } else {
      volume = isnode ? obj.fVolume : obj;
      subnodes = volume?.fNodes?.arr;
      shape = volume?.fShape;
   }

   if (!subnodes && (shape?._typename === clTGeoCompositeShape) && shape?.fNode) {
      if (!parent._childs) { // deepscan-disable-line
         createItem(parent, shape.fNode.fLeft, 'Left');
         createItem(parent, shape.fNode.fRight, 'Right');
      }

      return true;
   }

   if (!subnodes) return false;

   checkDuplicates(obj, subnodes);

   for (let i = 0; i < subnodes.length; ++i)
      createItem(parent, subnodes[i]);

   return true;
}


/** @summary find item with 3d painter
  * @private */
function findItemWithPainter(hitem, funcname) {
   while (hitem) {
      if (hitem._painter?._camera) {
         if (funcname && isFunc(hitem._painter[funcname]))
            hitem._painter[funcname]();
         return hitem;
      }
      hitem = hitem._parent;
   }
   return null;
}

/** @summary provide css style for geo object
  * @private */
function provideVisStyle(obj) {
   if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
      return obj.fRnrSelf ? ' geovis_this' : '';

   const vis = !testGeoBit(obj, geoBITS.kVisNone) && testGeoBit(obj, geoBITS.kVisThis);
   let chld = testGeoBit(obj, geoBITS.kVisDaughters);

   if (chld && !obj.fNodes?.arr?.length) chld = false;

   if (vis && chld) return ' geovis_all';
   if (vis) return ' geovis_this';
   if (chld) return ' geovis_daughters';
   return '';
}


/** @summary update icons
  * @private */
function updateBrowserIcons(obj, hpainter) {
   if (!obj || !hpainter) return;

   hpainter.forEachItem(m => {
      // update all items with that volume
      if ((obj === m._volume) || (obj === m._geoobj)) {
         m._icon = m._icon.split(' ')[0] + provideVisStyle(obj);
         hpainter.updateTreeNode(m);
      }
   });
}


/** @summary Return stack for the item from list of intersection
  * @private */
function getIntersectStack(item) {
   const obj = item?.object;
   if (!obj) return null;
   if (obj.stack)
      return obj.stack;
   if (obj.stacks && item.instanceId !== undefined && item.instanceId < obj.stacks.length)
      return obj.stacks[item.instanceId];
}

/**
  * @summary Toolbar for geometry painter
  *
  * @private
  */

class Toolbar {

   /** @summary constructor */
   constructor(container, bright, buttons) {
      this.bright = bright;
      this.buttons = buttons;
      this.element = container.append('div').attr('style', 'float: left; box-sizing: border-box; position: relative; bottom: 23px; vertical-align: middle; padding-left: 5px');
   }

   /** @summary add buttons */
   createButtons() {
      const buttonsNames = [];

      this.buttons.forEach(buttonConfig => {
         const buttonName = buttonConfig.name;
         if (!buttonName)
            throw new Error('must provide button name in button config');
         if (buttonsNames.indexOf(buttonName) !== -1)
            throw new Error(`button name ${buttonName} is taken`);

         buttonsNames.push(buttonName);

         const title = buttonConfig.title || buttonConfig.name;

         if (!isFunc(buttonConfig.click))
            throw new Error('must provide button click() function in button config');

         ToolbarIcons.createSVG(this.element, ToolbarIcons[buttonConfig.icon], 16, title, this.bright)
              .on('click', buttonConfig.click)
              .style('position', 'relative')
              .style('padding', '3px 1px');
      });
   }

   /** @summary change brightness */
   changeBrightness(bright) {
      if (this.bright === bright) return;
      this.element.selectAll('*').remove();
      this.bright = bright;
      this.createButtons();
   }

   /** @summary cleanup toolbar */
   cleanup() {
      this.element?.remove();
      delete this.element;
   }

} // class ToolBar


/**
  * @summary geometry drawing control
  *
  * @private
  */

class GeoDrawingControl extends InteractiveControl {

   constructor(mesh, bloom) {
      super();
      this.mesh = mesh?.material ? mesh : null;
      this.bloom = bloom;
   }

   /** @summary set highlight */
   setHighlight(col, indx) {
      return this.drawSpecial(col, indx);
   }

   /** @summary draw special */
   drawSpecial(col, indx) {
      const c = this.mesh;
      if (!c?.material) return;

      if (c.isInstancedMesh) {
         if (c._highlight_mesh) {
            c.remove(c._highlight_mesh);
            delete c._highlight_mesh;
         }

         if (col && indx !== undefined) {
            const h = new Mesh(c.geometry, c.material.clone());

            if (this.bloom) {
               h.layers.enable(_BLOOM_SCENE);
               h.material.emissive = new Color(0x00ff00);
            } else {
               h.material.color = new Color(col);
               h.material.opacity = 1.0;
            }
            const m = new Matrix4();
            c.getMatrixAt(indx, m);
            h.applyMatrix4(m);
            c.add(h);

            h.jsroot_special = true; // exclude from intersections

            c._highlight_mesh = h;
         }
         return true;
      }

      if (col) {
         if (!c.origin) {
            c.origin = {
              color: c.material.color,
              emissive: c.material.emissive,
              opacity: c.material.opacity,
              width: c.material.linewidth,
              size: c.material.size
           };
         }
         if (this.bloom) {
            c.layers.enable(_BLOOM_SCENE);
            c.material.emissive = new Color(0x00ff00);
         } else {
            c.material.color = new Color(col);
            c.material.opacity = 1.0;
         }

         if (c.hightlightWidthScale && !browser.isWin)
            c.material.linewidth = c.origin.width * c.hightlightWidthScale;
         if (c.highlightScale)
            c.material.size = c.origin.size * c.highlightScale;
         return true;
      } else if (c.origin) {
         if (this.bloom) {
            c.material.emissive = c.origin.emissive;
            c.layers.enable(_ENTIRE_SCENE);
         } else {
            c.material.color = c.origin.color;
            c.material.opacity = c.origin.opacity;
         }
         if (c.hightlightWidthScale)
            c.material.linewidth = c.origin.width;
         if (c.highlightScale)
            c.material.size = c.origin.size;
         return true;
      }
   }

} // class GeoDrawingControl


const stageInit = 0, stageCollect = 1, stageWorkerCollect = 2, stageAnalyze = 3, stageCollShapes = 4,
      stageStartBuild = 5, stageWorkerBuild = 6, stageBuild = 7, stageBuildReady = 8, stageWaitMain = 9, stageBuildProj = 10;

/**
 * @summary Painter class for geometries drawing
 *
 * @private
 */

class TGeoPainter extends ObjectPainter {

   /** @summary Constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} obj - supported TGeo object */
   constructor(dom, obj) {
      let gm;
      if (obj?._typename === clTGeoManager) {
         gm = obj;
         obj = obj.fMasterVolume;
      }

      if (obj?._typename && (obj._typename.indexOf(clTGeoVolume) === 0))
         obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };

      super(dom, obj);

      if (getHistPainter3DCfg(this.getMainPainter()))
         this.superimpose = true;

      if (gm) this.geo_manager = gm;

      this.no_default_title = true; // do not set title to main DIV
      this.mode3d = true; // indication of 3D mode
      this.drawing_stage = stageInit; //
      this.drawing_log = 'Init';
      this.ctrl = {
         clipIntersect: true,
         clipVisualize: false,
         clip: [{ name: 'x', enabled: false, value: 0, min: -100, max: 100, step: 1 },
                { name: 'y', enabled: false, value: 0, min: -100, max: 100, step: 1 },
                { name: 'z', enabled: false, value: 0, min: -100, max: 100, step: 1 }],
         _highlight: 0,
         highlight: 0,
         highlight_bloom: 0,
         highlight_scene: 0,
         highlight_color: '#00ff00',
         bloom_strength: 1.5,
         more: 1,
         maxfaces: 0,
         vislevel: undefined,
         maxnodes: undefined,
         dflt_colors: false,

         info: { num_meshes: 0, num_faces: 0, num_shapes: 0 },
         depthTest: true,
         depthMethod: 'dflt',
         select_in_view: false,
         update_browser: true,
         use_fog: false,
         light: { kind: 'points', top: false, bottom: false, left: false, right: false, front: false, specular: true, power: 1 },
         lightKindItems: [
            { name: 'AmbientLight', value: 'ambient' },
            { name: 'DirectionalLight', value: 'points' },
            { name: 'HemisphereLight', value: 'hemisphere' },
            { name: 'Ambient + Point', value: 'mix' }
         ],
         trans_radial: 0,
         trans_z: 0,
         scale: new Vector3(1, 1, 1),
         zoom: 1.0, rotatey: 0, rotatez: 0,
         depthMethodItems: [
            { name: 'Default', value: 'dflt' },
            { name: 'Raytraicing', value: 'ray' },
            { name: 'Boundary box', value: 'box' },
            { name: 'Mesh size', value: 'size' },
            { name: 'Central point', value: 'pnt' }
          ],
          cameraKindItems: [
            { name: 'Perspective', value: 'perspective' },
            { name: 'Perspective (Floor XOZ)', value: 'perspXOZ' },
            { name: 'Perspective (Floor YOZ)', value: 'perspYOZ' },
            { name: 'Perspective (Floor XOY)', value: 'perspXOY' },
            { name: 'Orthographic (XOY)', value: 'orthoXOY' },
            { name: 'Orthographic (XOZ)', value: 'orthoXOZ' },
            { name: 'Orthographic (ZOY)', value: 'orthoZOY' },
            { name: 'Orthographic (ZOX)', value: 'orthoZOX' },
            { name: 'Orthographic (XnOY)', value: 'orthoXNOY' },
            { name: 'Orthographic (XnOZ)', value: 'orthoXNOZ' },
            { name: 'Orthographic (ZnOY)', value: 'orthoZNOY' },
            { name: 'Orthographic (ZnOX)', value: 'orthoZNOX' }
         ],
         cameraOverlayItems: [
            { name: 'None', value: 'none' },
            { name: 'Bar', value: 'bar' },
            { name: 'Axis', value: 'axis' },
            { name: 'Grid', value: 'grid' },
            { name: 'Grid background', value: 'gridb' },
            { name: 'Grid foreground', value: 'gridf' }
         ],
         camera_kind: 'perspective',
         camera_overlay: 'gridb',
         rotate: false,
         background: settings.DarkMode ? '#000000' : '#ffffff',
         can_rotate: true,
         _axis: 0,
         instancing: 0,
         _count: false,
         // material properties
         wireframe: false,
         transparency: 0,
         flatShading: false,
         roughness: 0.5,
         metalness: 0.5,
         shininess: 0,
         reflectivity: 0.5,
         material_kind: 'lambert',
         materialKinds: [
            { name: 'MeshLambertMaterial', value: 'lambert', emissive: true, props: [{ name: 'flatShading' }] },
            { name: 'MeshBasicMaterial', value: 'basic' },
            { name: 'MeshStandardMaterial', value: 'standard', emissive: true,
                props: [{ name: 'flatShading' }, { name: 'roughness', min: 0, max: 1, step: 0.001 }, { name: 'metalness', min: 0, max: 1, step: 0.001 }] },
            { name: 'MeshPhysicalMaterial', value: 'physical', emissive: true,
               props: [{ name: 'flatShading' }, { name: 'roughness', min: 0, max: 1, step: 0.001 }, { name: 'metalness', min: 0, max: 1, step: 0.001 }, { name: 'reflectivity', min: 0, max: 1, step: 0.001 }] },
            { name: 'MeshPhongMaterial', value: 'phong', emissive: true,
                props: [{ name: 'flatShading' }, { name: 'shininess', min: 0, max: 100, step: 0.1 }] },
            { name: 'MeshNormalMaterial', value: 'normal', props: [{ name: 'flatShading' }] },
            { name: 'MeshDepthMaterial', value: 'depth' },
            { name: 'MeshMatcapMaterial', value: 'matcap' },
            { name: 'MeshToonMaterial', value: 'toon' }
         ],
         getMaterialCfg: function() {
             let cfg;
             this.materialKinds.forEach(item => {
                if (item.value === this.material_kind)
                   cfg = item;
             });
             return cfg;
         }
      };

      this.cleanup(true);
   }

   /** @summary Function callled by framework when dark mode is changed
     * @private */
   changeDarkMode(mode) {
      if ((this.ctrl.background === '#000000') || (this.ctrl.background === '#ffffff'))
         this.changedBackground((mode ?? settings.DarkMode) ? '#000000' : '#ffffff');
   }

   /** @summary Change drawing stage
     * @private */
   changeStage(value, msg) {
      this.drawing_stage = value;
      if (!msg) {
         switch (value) {
            case stageInit: msg = 'Building done'; break;
            case stageCollect: msg = 'collect visibles'; break;
            case stageWorkerCollect: msg = 'worker collect visibles'; break;
            case stageAnalyze: msg = 'Analyse visibles'; break;
            case stageCollShapes: msg = 'collect shapes for building'; break;
            case stageStartBuild: msg = 'Start build shapes'; break;
            case stageWorkerBuild: msg = 'Worker build shapes'; break;
            case stageBuild: msg = 'Build shapes'; break;
            case stageBuildReady: msg = 'Build ready'; break;
            case stageWaitMain: msg = 'Wait for main painter'; break;
            case stageBuildProj: msg = 'Build projection'; break;
            default: msg = `stage ${value}`;
         }
      }
      this.drawing_log = msg;
   }

   /** @summary Check drawing stage */
   isStage(value) { return value === this.drawing_stage; }

   isBatchMode() { return isBatchMode() || this.batch_mode; }

   /** @summary Create toolbar */
   createToolbar() {
      if (this._toolbar || !this._webgl || this.ctrl.notoolbar || this.isBatchMode()) return;
      const buttonList = [{
         name: 'toImage',
         title: 'Save as PNG',
         icon: 'camera',
         click: () => this.createSnapshot()
      }, {
         name: 'control',
         title: 'Toggle control UI',
         icon: 'rect',
         click: () => this.showControlGui('toggle')
      }, {
         name: 'enlarge',
         title: 'Enlarge geometry drawing',
         icon: 'circle',
         click: () => this.toggleEnlarge()
      }];

      // Only show VR icon if WebVR API available.
      if (navigator.getVRDisplays) {
         buttonList.push({
            name: 'entervr',
            title: 'Enter VR (It requires a VR Headset connected)',
            icon: 'vrgoggles',
            click: () => this.toggleVRMode()
         });
         this.initVRMode();
      }

      if (settings.ContextMenu) {
         buttonList.push({
            name: 'menu',
            title: 'Show context menu',
            icon: 'question',
            click: evnt => {
               evnt.preventDefault();
               evnt.stopPropagation();

               if (closeMenu()) return;

               createMenu(evnt, this).then(menu => {
                   menu.painter.fillContextMenu(menu);
                   menu.show();
               });
            }
         });
      }

      const bkgr = new Color(this.ctrl.background);

      this._toolbar = new Toolbar(this.selectDom(), (bkgr.r + bkgr.g + bkgr.b) < 1, buttonList);

      this._toolbar.createButtons();
   }

   /** @summary Initialize VR mode */
   initVRMode() {
      // Dolly contains camera and controllers in VR Mode
      // Allows moving the user in the scene
      this._dolly = new Group();
      this._scene.add(this._dolly);
      this._standingMatrix = new Matrix4();

      // Raycaster temp variables to avoid one per frame allocation.
      this._raycasterEnd = new Vector3();
      this._raycasterOrigin = new Vector3();

      navigator.getVRDisplays().then(displays => {
         const vrDisplay = displays[0];
         if (!vrDisplay) return;
         this._renderer.vr.setDevice(vrDisplay);
         this._vrDisplay = vrDisplay;
         if (vrDisplay.stageParameters)
            this._standingMatrix.fromArray(vrDisplay.stageParameters.sittingToStandingTransform);

         this.initVRControllersGeometry();
      });
   }

   /** @summary Init VR controllers geometry
     * @private */
   initVRControllersGeometry() {
      const geometry = new SphereGeometry(0.025, 18, 36),
          material = new MeshBasicMaterial({ color: 'grey', vertexColors: false }),
          rayMaterial = new MeshBasicMaterial({ color: 'fuchsia', vertexColors: false }),
          rayGeometry = new BoxGeometry(0.001, 0.001, 2),
          ray1Mesh = new Mesh(rayGeometry, rayMaterial),
          ray2Mesh = new Mesh(rayGeometry, rayMaterial),
          sphere1 = new Mesh(geometry, material),
          sphere2 = new Mesh(geometry, material);

      this._controllersMeshes = [];
      this._controllersMeshes.push(sphere1);
      this._controllersMeshes.push(sphere2);
      ray1Mesh.position.z -= 1;
      ray2Mesh.position.z -= 1;
      sphere1.add(ray1Mesh);
      sphere2.add(ray2Mesh);
      this._dolly.add(sphere1);
      this._dolly.add(sphere2);
      // Controller mesh hidden by default
      sphere1.visible = false;
      sphere2.visible = false;
   }

   /** @summary Update VR controllers list
     * @private */
   updateVRControllersList() {
      const gamepads = navigator.getGamepads && navigator.getGamepads();
      // Has controller list changed?
      if (this.vrControllers && (gamepads.length === this.vrControllers.length)) return;
      // Hide meshes.
      this._controllersMeshes.forEach(mesh => { mesh.visible = false; });
      this._vrControllers = [];
      for (let i = 0; i < gamepads.length; ++i) {
         if (!gamepads[i] || !gamepads[i].pose) continue;
         this._vrControllers.push({
            gamepad: gamepads[i],
            mesh: this._controllersMeshes[i]
         });
         this._controllersMeshes[i].visible = true;
      }
   }

   /** @summary Process VR controller intersection
     * @private */
   processVRControllerIntersections() {
      let intersects = [];
      for (let i = 0; i < this._vrControllers.length; ++i) {
         const controller = this._vrControllers[i].mesh,
               end = controller.localToWorld(this._raycasterEnd.set(0, 0, -1)),
               origin = controller.localToWorld(this._raycasterOrigin.set(0, 0, 0));
         end.sub(origin).normalize();
         intersects = intersects.concat(this._controls.getOriginDirectionIntersects(origin, end));
      }
      // Remove duplicates.
      intersects = intersects.filter((item, pos) => { return intersects.indexOf(item) === pos; });
      this._controls.processMouseMove(intersects);
   }

   /** @summary Update VR controllers
     * @private */
   updateVRControllers() {
      this.updateVRControllersList();
      // Update pose.
      for (let i = 0; i < this._vrControllers.length; ++i) {
         const controller = this._vrControllers[i],
               orientation = controller.gamepad.pose.orientation,
               position = controller.gamepad.pose.position,
               controllerMesh = controller.mesh;
         if (orientation) controllerMesh.quaternion.fromArray(orientation);
         if (position) controllerMesh.position.fromArray(position);
         controllerMesh.updateMatrix();
         controllerMesh.applyMatrix4(this._standingMatrix);
         controllerMesh.matrixWorldNeedsUpdate = true;
      }
      this.processVRControllerIntersections();
   }

   /** @summary Toggle VR mode
     * @private */
   toggleVRMode() {
      if (!this._vrDisplay) return;
      // Toggle VR mode off
      if (this._vrDisplay.isPresenting) {
         this.exitVRMode();
         return;
      }
      this._previousCameraPosition = this._camera.position.clone();
      this._previousCameraRotation = this._camera.rotation.clone();
      this._vrDisplay.requestPresent([{ source: this._renderer.domElement }]).then(() => {
         this._previousCameraNear = this._camera.near;
         this._dolly.position.set(this._camera.position.x/4, -this._camera.position.y/8, -this._camera.position.z/4);
         this._camera.position.set(0, 0, 0);
         this._dolly.add(this._camera);
         this._camera.near = 0.1;
         this._camera.updateProjectionMatrix();
         this._renderer.vr.enabled = true;
         this._renderer.setAnimationLoop(() => {
            this.updateVRControllers();
            this.render3D(0);
         });
      });
      this._renderer.vr.enabled = true;

      window.addEventListener('keydown', evnt => {
         // Esc Key turns VR mode off
         if (evnt.code === 'Escape') this.exitVRMode();
      });
   }

   /** @summary Exit VR mode
     * @private */
   exitVRMode() {
      if (!this._vrDisplay.isPresenting) return;
      this._renderer.vr.enabled = false;
      this._dolly.remove(this._camera);
      this._scene.add(this._camera);
      // Restore Camera pose
      this._camera.position.copy(this._previousCameraPosition);
      this._previousCameraPosition = undefined;
      this._camera.rotation.copy(this._previousCameraRotation);
      this._previousCameraRotation = undefined;
      this._camera.near = this._previousCameraNear;
      this._camera.updateProjectionMatrix();
      this._vrDisplay.exitPresent();
   }

   /** @summary Returns main geometry object */
   getGeometry() {
      return this.getObject();
   }

   /** @summary Modify visibility of provided node by name */
   modifyVisisbility(name, sign) {
      if (getNodeKind(this.getGeometry()) !== 0) return;

      if (!name)
         return setGeoBit(this.getGeometry().fVolume, geoBITS.kVisThis, (sign === '+'));

      let regexp, exact = false;

      // arg.node.fVolume
      if (name.indexOf('*') < 0) {
         regexp = new RegExp('^'+name+'$');
         exact = true;
      } else {
         regexp = new RegExp('^' + name.split('*').join('.*') + '$');
         exact = false;
      }

      this.findNodeWithVolume(regexp, arg => {
         setInvisibleAll(arg.node.fVolume, (sign !== '+'));
         return exact ? arg : null; // continue search if not exact expression provided
      });
   }

   /** @summary Decode drawing options */
   decodeOptions(opt) {
      if (!isStr(opt)) opt = '';

      if (this.superimpose && (opt.indexOf('same') === 0))
         opt = opt.slice(4);

      const res = this.ctrl,

       macro = opt.indexOf('macro:');
      if (macro >= 0) {
         let separ = opt.indexOf(';', macro+6);
         if (separ < 0) separ = opt.length;
         res.script_name = opt.slice(macro+6, separ);
         opt = opt.slice(0, macro) + opt.slice(separ+1);
         console.log(`script ${res.script_name} rest ${opt}`);
      }

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

         let p2 = p1+1;
         const regexp = /[,; .]/;
         while ((p2 < opt.length) && !regexp.test(opt[p2]) && (opt[p2] !== '+') && (opt[p2] !== '-')) p2++;

         const name = opt.substring(p1+1, p2);
         opt = opt.slice(0, p1) + opt.slice(p2);

         this.modifyVisisbility(name, sign);
      }

      const d = new DrawOptions(opt);

      if (d.check('MAIN')) res.is_main = true;

      if (d.check('TRACKS')) res.tracks = true; // only for TGeoManager
      if (d.check('SHOWTOP')) res.showtop = true; // only for TGeoManager
      if (d.check('NO_SCREEN')) res.no_screen = true; // ignore kVisOnScreen bits for visibility

      if (d.check('NOINSTANCING')) res.instancing = -1; // disable usage of InstancedMesh
      if (d.check('INSTANCING')) res.instancing = 1; // force usage of InstancedMesh

      if (d.check('ORTHO_CAMERA')) { res.camera_kind = 'orthoXOY'; res.can_rotate = 0; }
      if (d.check('ORTHO', true)) { res.camera_kind = 'ortho' + d.part; res.can_rotate = 0; }
      if (d.check('OVERLAY', true)) res.camera_overlay = d.part.toLowerCase();
      if (d.check('CAN_ROTATE')) res.can_rotate = true;
      if (d.check('PERSPECTIVE')) { res.camera_kind = 'perspective'; res.can_rotate = true; }
      if (d.check('PERSP', true)) { res.camera_kind = 'persp' + d.part; res.can_rotate = true; }
      if (d.check('MOUSE_CLICK')) res.mouse_click = true;

      if (d.check('DEPTHRAY') || d.check('DRAY')) res.depthMethod = 'ray';
      if (d.check('DEPTHBOX') || d.check('DBOX')) res.depthMethod = 'box';
      if (d.check('DEPTHPNT') || d.check('DPNT')) res.depthMethod = 'pnt';
      if (d.check('DEPTHSIZE') || d.check('DSIZE')) res.depthMethod = 'size';
      if (d.check('DEPTHDFLT') || d.check('DDFLT')) res.depthMethod = 'dflt';

      if (d.check('ZOOM', true)) res.zoom = d.partAsFloat(0, 100) / 100;
      if (d.check('ROTY', true)) res.rotatey = d.partAsFloat();
      if (d.check('ROTZ', true)) res.rotatez = d.partAsFloat();

      if (d.check('PHONG')) res.material_kind = 'phong';
      if (d.check('LAMBERT')) res.material_kind = 'lambert';
      if (d.check('MATCAP')) res.material_kind = 'matcap';
      if (d.check('TOON')) res.material_kind = 'toon';

      if (d.check('AMBIENT')) res.light.kind = 'ambient';

      const getCamPart = () => {
         let neg = 1;
         if (d.part[0] === 'N') {
            neg = -1;
            d.part = d.part.slice(1);
         }
         return neg * d.partAsFloat();
      };

      if (d.check('CAMX', true)) res.camx = getCamPart();
      if (d.check('CAMY', true)) res.camy = getCamPart();
      if (d.check('CAMZ', true)) res.camz = getCamPart();
      if (d.check('CAMLX', true)) res.camlx = getCamPart();
      if (d.check('CAMLY', true)) res.camly = getCamPart();
      if (d.check('CAMLZ', true)) res.camlz = getCamPart();

      if (d.check('BLACK')) res.background = '#000000';
      if (d.check('WHITE')) res.background = '#FFFFFF';

      if (d.check('BKGR_', true)) {
         let bckgr = null;
         if (d.partAsInt(1) > 0)
            bckgr = getColor(d.partAsInt());
          else {
            for (let col = 0; col < 8; ++col) {
               if (getColor(col).toUpperCase() === d.part)
                  bckgr = getColor(col);
            }
         }
         if (bckgr) res.background = '#' + new Color(bckgr).getHexString();
      }

      if (d.check('R3D_', true))
         res.Render3D = constants.Render3D.fromString(d.part.toLowerCase());

      if (d.check('MORE', true)) res.more = d.partAsInt(0, 2) ?? 2;
      if (d.check('ALL')) { res.more = 100; res.vislevel = 99; }

      if (d.check('VISLVL', true)) res.vislevel = d.partAsInt();
      if (d.check('MAXNODES', true)) res.maxnodes = d.partAsInt();
      if (d.check('MAXFACES', true)) res.maxfaces = d.partAsInt();

      if (d.check('CONTROLS') || d.check('CTRL')) res.show_controls = true;

      if (d.check('CLIPXYZ')) res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;
      if (d.check('CLIPX')) res.clip[0].enabled = true;
      if (d.check('CLIPY')) res.clip[1].enabled = true;
      if (d.check('CLIPZ')) res.clip[2].enabled = true;
      if (d.check('CLIP')) res.clip[0].enabled = res.clip[1].enabled = res.clip[2].enabled = true;

      if (d.check('PROJX', true)) { res.project = 'x'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }
      if (d.check('PROJY', true)) { res.project = 'y'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }
      if (d.check('PROJZ', true)) { res.project = 'z'; if (d.partAsInt(1) > 0) res.projectPos = d.partAsInt(); res.can_rotate = 0; }

      if (d.check('DFLT_COLORS') || d.check('DFLT')) res.dflt_colors = true;
      d.check('SSAO'); // deprecated
      if (d.check('NOBLOOM')) res.highlight_bloom = false;
      if (d.check('BLOOM')) res.highlight_bloom = true;
      if (d.check('OUTLINE')) res.outline = true;

      if (d.check('NOWORKER')) res.use_worker = -1;
      if (d.check('WORKER')) res.use_worker = 1;

      if (d.check('NOFOG')) res.use_fog = false;
      if (d.check('FOG')) res.use_fog = true;

      if (d.check('NOHIGHLIGHT') || d.check('NOHIGH')) res.highlight_scene = res.highlight = false;
      if (d.check('HIGHLIGHT')) res.highlight_scene = res.highlight = true;
      if (d.check('HSCENEONLY')) { res.highlight_scene = true; res.highlight = false; }
      if (d.check('NOHSCENE')) res.highlight_scene = false;
      if (d.check('HSCENE')) res.highlight_scene = true;

      if (d.check('WIREFRAME') || d.check('WIRE')) res.wireframe = true;
      if (d.check('ROTATE')) res.rotate = true;

      if (d.check('INVX') || d.check('INVERTX')) res.scale.x = -1;
      if (d.check('INVY') || d.check('INVERTY')) res.scale.y = -1;
      if (d.check('INVZ') || d.check('INVERTZ')) res.scale.z = -1;

      if (d.check('COUNT')) res._count = true;

      if (d.check('TRANSP', true))
         res.transparency = d.partAsInt(0, 100)/100;

      if (d.check('OPACITY', true))
         res.transparency = 1 - d.partAsInt(0, 100)/100;

      if (d.check('AXISCENTER') || d.check('AXISC') || d.check('AC')) res._axis = 2;
      if (d.check('AXIS') || d.check('A')) res._axis = 1;

      if (d.check('TRR', true)) res.trans_radial = d.partAsInt()/100;
      if (d.check('TRZ', true)) res.trans_z = d.partAsInt()/100;


      if (d.check('W')) res.wireframe = true;
      if (d.check('Y')) res._yup = true;
      if (d.check('Z')) res._yup = false;

      // when drawing geometry without TCanvas, yup = true by default
      if (res._yup === undefined)
         res._yup = this.getCanvSvg().empty();

      // let reuse for storing origin options
      this.options = res;
   }

   /** @summary Activate specified items in the browser */
   activateInBrowser(names, force) {
      if (isStr(names)) names = [names];

      if (this._hpainter) {
         // show browser if it not visible

         this._hpainter.activateItems(names, force);

         // if highlight in the browser disabled, suppress in few seconds
         if (!this.ctrl.update_browser)
            setTimeout(() => this._hpainter.activateItems([]), 2000);
      }
   }

   /** @summary  method used to check matrix calculations performance with current three.js model */
   testMatrixes() {
      let errcnt = 0, totalcnt = 0, totalmax = 0;

      const arg = {
            domatrix: true,
            func: (/* node */) => {
               let m2 = this.getmatrix();
               const entry = this.copyStack(),
                     mesh = this._clones.createObject3D(entry.stack, this._toplevel, 'mesh');
               if (!mesh) return true;

               totalcnt++;

               const m1 = mesh.matrixWorld;
               if (m1.equals(m2)) return true;
               if ((m1.determinant() > 0) && (m2.determinant() < -0.9)) {
                  const flip = new Vector3(1, 1, -1);
                  m2 = m2.clone().scale(flip);
                  if (m1.equals(m2)) return true;
               }

               let max = 0;
               for (let k = 0; k < 16; ++k)
                  max = Math.max(max, Math.abs(m1.elements[k] - m2.elements[k]));

               totalmax = Math.max(max, totalmax);

               if (max < 1e-4) return true;

               console.log(`${this._clones.resolveStack(entry.stack).name} maxdiff ${max} determ ${m1.determinant()} ${m2.determinant()}`);

               errcnt++;

               return false;
            }
         },

       tm1 = new Date().getTime();

      /* let cnt = */ this._clones.scanVisible(arg);

      const tm2 = new Date().getTime();

      console.log(`Compare matrixes total ${totalcnt} errors ${errcnt} takes ${tm2-tm1} maxdiff ${totalmax}`);
   }

   /** @summary Fill context menu */
   fillContextMenu(menu) {
      menu.add('header: Draw options');

      menu.addchk(this.ctrl.update_browser, 'Browser update', () => {
         this.ctrl.update_browser = !this.ctrl.update_browser;
         if (!this.ctrl.update_browser) this.activateInBrowser([]);
      });
      menu.addchk(this.ctrl.show_controls, 'Show Controls', () => this.showControlGui('toggle'));

      menu.add('sub:Show axes', () => this.setAxesDraw('toggle'));
      menu.addchk(this.ctrl._axis === 0, 'off', 0, arg => this.setAxesDraw(parseInt(arg)));
      menu.addchk(this.ctrl._axis === 1, 'side', 1, arg => this.setAxesDraw(parseInt(arg)));
      menu.addchk(this.ctrl._axis === 2, 'center', 2, arg => this.setAxesDraw(parseInt(arg)));
      menu.add('endsub:');

      if (this.geo_manager)
         menu.addchk(this.ctrl.showtop, 'Show top volume', () => this.setShowTop(!this.ctrl.showtop));

      menu.addchk(this.ctrl.wireframe, 'Wire frame', () => this.toggleWireFrame());

      if (!this.getCanvPainter())
         menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));

      menu.add('sub:Highlight');

      menu.addchk(!this.ctrl.highlight, 'Off', () => {
         this.ctrl.highlight = false;
         this.changedHighlight();
      });
      menu.addchk(this.ctrl.highlight && !this.ctrl.highlight_bloom, 'Normal', () => {
         this.ctrl.highlight = true;
         this.ctrl.highlight_bloom = false;
         this.changedHighlight();
      });
      menu.addchk(this.ctrl.highlight && this.ctrl.highlight_bloom, 'Bloom', () => {
         this.ctrl.highlight = true;
         this.ctrl.highlight_bloom = true;
         this.changedHighlight();
      });

      menu.add('separator');

      menu.addchk(this.ctrl.highlight_scene, 'Scene', flag => {
         this.ctrl.highlight_scene = flag;
         this.changedHighlight();
      });

      menu.add('endsub:');

      menu.add('sub:Camera');
      menu.add('Reset position', () => this.focusCamera());
      if (!this.ctrl.project)
          menu.addchk(this.ctrl.rotate, 'Autorotate', () => this.setAutoRotate(!this.ctrl.rotate));

      if (!this._geom_viewer) {
         menu.addchk(this.canRotateCamera(), 'Can rotate', () => this.changeCanRotate(!this.ctrl.can_rotate));

         menu.add('Get position', () => menu.info('Position (as url)', '&opt=' + this.produceCameraUrl()));
         if (!this.isOrthoCamera()) {
            menu.add('Absolute position', () => {
               const url = this.produceCameraUrl(true), p = url.indexOf('camlx');
               menu.info('Position (as url)', '&opt=' + ((p < 0) ? url : url.slice(0, p) + '\n' + url.slice(p)));
            });
         }

         menu.add('sub:Kind');
         this.ctrl.cameraKindItems.forEach(item =>
            menu.addchk(this.ctrl.camera_kind === item.value, item.name, item.value, arg => {
               this.ctrl.camera_kind = arg;
               this.changeCamera();
            }));
         menu.add('endsub:');

         if (this.isOrthoCamera()) {
            menu.add('sub:Overlay');
            this.ctrl.cameraOverlayItems.forEach(item =>
               menu.addchk(this.ctrl.camera_overlay === item.value, item.name, item.value, arg => {
                  this.ctrl.camera_overlay = arg;
                  this.changeCamera();
               }));
            menu.add('endsub:');
         }
      }
      menu.add('endsub:');

      menu.addchk(this.ctrl.select_in_view, 'Select in view', () => {
         this.ctrl.select_in_view = !this.ctrl.select_in_view;
         if (this.ctrl.select_in_view) this.startDrawGeometry();
      });
   }

   /** @summary Method used to set transparency for all geometrical shapes
     * @param {number|Function} transparency - one could provide function
     * @param {boolean} [skip_render] - if specified, do not perform rendering */
   changedGlobalTransparency(transparency) {
      const func = isFunc(transparency) ? transparency : null;
      if (func || (transparency === undefined))
         transparency = this.ctrl.transparency;

      this._toplevel?.traverse(node => {
         // ignore all kind of extra elements
         if (node?.material?.inherentOpacity === undefined)
            return;

         const t = func ? func(node) : undefined;
         if (t !== undefined)
            node.material.opacity = 1 - t;
         else
            node.material.opacity = Math.min(1 - (transparency || 0), node.material.inherentOpacity);

         node.material.depthWrite = node.material.opacity === 1;
         node.material.transparent = node.material.opacity < 1;
      });

      this.render3D();
   }

   /** @summary Method used to interactively change material kinds */
   changedMaterial() {
      this._toplevel?.traverse(node => {
         // ignore all kind of extra elements
         if (node.material?.inherentArgs !== undefined)
            node.material = createMaterial(this.ctrl, node.material.inherentArgs);
      });

      this.render3D(-1);
   }

   /** @summary Change for all materials that property */
   changeMaterialProperty(name) {
      const value = this.ctrl[name];
      if (value === undefined)
         return console.error('No property ', name);

      this._toplevel?.traverse(node => {
         // ignore all kind of extra elements
         if (node.material?.inherentArgs === undefined) return;

         if (node.material[name] !== undefined) {
            node.material[name] = value;
            node.material.needsUpdate = true;
         }
      });

      this.render3D();
   }

   /** @summary Reset transformation */
   resetTransformation() {
      this.changedTransformation('reset');
   }

   /** @summary Method should be called when transformation parameters were changed */
   changedTransformation(arg) {
      if (!this._toplevel) return;

      const ctrl = this.ctrl,
          translation = new Matrix4(),
          vect2 = new Vector3();

      if (arg === 'reset')
         ctrl.trans_z = ctrl.trans_radial = 0;

      this._toplevel.traverse(mesh => {
         if (mesh.stack !== undefined) {
            const node = mesh.parent;

            if (arg === 'reset') {
               if (node.matrix0) {
                  node.matrix.copy(node.matrix0);
                  node.matrix.decompose(node.position, node.quaternion, node.scale);
                  node.matrixWorldNeedsUpdate = true;
               }
               delete node.matrix0;
               delete node.vect0;
               delete node.vect1;
               delete node.minvert;
               return;
            }

            if (node.vect0 === undefined) {
               node.matrix0 = node.matrix.clone();
               node.minvert = new Matrix4().copy(node.matrixWorld).invert();

               const box3 = getBoundingBox(mesh, null, true),
                   signz = mesh._flippedMesh ? -1 : 1;

               // real center of mesh in local coordinates
               node.vect0 = new Vector3((box3.max.x + box3.min.x) / 2, (box3.max.y + box3.min.y) / 2, signz * (box3.max.z + box3.min.z) / 2).applyMatrix4(node.matrixWorld);
               node.vect1 = new Vector3(0, 0, 0).applyMatrix4(node.minvert);
            }

            vect2.set(ctrl.trans_radial * node.vect0.x, ctrl.trans_radial * node.vect0.y, ctrl.trans_z * node.vect0.z).applyMatrix4(node.minvert).sub(node.vect1);

            node.matrix.multiplyMatrices(node.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));
            node.matrix.decompose(node.position, node.quaternion, node.scale);
            node.matrixWorldNeedsUpdate = true;
         } else if (mesh.stacks !== undefined) {
            mesh.instanceMatrix.needsUpdate = true;

            if (arg === 'reset') {
               mesh.trans?.forEach((item, i) => {
                  mesh.setMatrixAt(i, item.matrix0);
               });
               delete mesh.trans;
               return;
            }

            if (mesh.trans === undefined) {
               mesh.trans = new Array(mesh.count);

               mesh.geometry.computeBoundingBox();

               for (let i = 0; i < mesh.count; i++) {
                  const item = {
                     matrix0: new Matrix4(),
                     minvert: new Matrix4()
                  };

                  mesh.trans[i] = item;

                  mesh.getMatrixAt(i, item.matrix0);
                  item.minvert.copy(item.matrix0).invert();

                  const box3 = new Box3().copy(mesh.geometry.boundingBox).applyMatrix4(item.matrix0);

                  item.vect0 = new Vector3((box3.max.x + box3.min.x) / 2, (box3.max.y + box3.min.y) / 2, (box3.max.z + box3.min.z) / 2);
                  item.vect1 = new Vector3(0, 0, 0).applyMatrix4(item.minvert);
               }
            }

            const mm = new Matrix4();

            mesh.trans?.forEach((item, i) => {
               vect2.set(ctrl.trans_radial * item.vect0.x, ctrl.trans_radial * item.vect0.y, ctrl.trans_z * item.vect0.z).applyMatrix4(item.minvert).sub(item.vect1);

               mm.multiplyMatrices(item.matrix0, translation.makeTranslation(vect2.x, vect2.y, vect2.z));

               mesh.setMatrixAt(i, mm);
            });
         }
      });

      this._toplevel.updateMatrixWorld();

      // axes drawing always triggers rendering
      if (arg !== 'norender')
         this.drawAxesAndOverlay();
   }

   /** @summary Should be called when autorotate property changed */
   changedAutoRotate() {
      this.autorotate(2.5);
   }

   /** @summary Method should be called when changing axes drawing */
   changedAxes() {
      if (isStr(this.ctrl._axis))
         this.ctrl._axis = parseInt(this.ctrl._axis);

      this.drawAxesAndOverlay();
   }

   /** @summary Method should be called to change background color */
   changedBackground(val) {
      if (val !== undefined)
         this.ctrl.background = val;
      this._scene.background = new Color(this.ctrl.background);
      this._renderer.setClearColor(this._scene.background, 1);
      this.render3D(0);

      if (this._toolbar) {
         const bkgr = new Color(this.ctrl.background);
         this._toolbar.changeBrightness((bkgr.r + bkgr.g + bkgr.b) < 1);
      }
   }

   /** @summary Display control GUI */
   showControlGui(on) {
      // while complete geo drawing can be removed until dat is loaded - just check and ignore callback
      if (!this.ctrl) return;

      if (on === 'toggle')
         on = !this._gui;
       else if (on === undefined)
         on = this.ctrl.show_controls;


      this.ctrl.show_controls = on;

      if (this._gui) {
         if (!on) {
            this._gui.destroy();
            delete this._gui;
         }
         return;
      }

      if (!on || !this._renderer)
         return;


      const main = this.selectDom();
      if (main.style('position') === 'static')
         main.style('position', 'relative');

      this._gui = new GUI({ container: main.node(), closeFolders: true, width: Math.min(300, this._scene_width / 2),
                            title: 'Settings' });

      const dom = this._gui.domElement;
      dom.style.position = 'absolute';
      dom.style.top = 0;
      dom.style.right = 0;

      this._gui.painter = this;

      const makeLil = items => {
         const lil = {};
         items.forEach(i => { lil[i.name] = i.value; });
         return lil;
      };

      if (!this.ctrl.project) {
         const selection = this._gui.addFolder('Selection');

         if (!this.ctrl.maxnodes)
            this.ctrl.maxnodes = this._clones?.getMaxVisNodes() ?? 10000;
         if (!this.ctrl.vislevel)
            this.ctrl.vislevel = this._clones?.getVisLevel() ?? 3;
         if (!this.ctrl.maxfaces)
            this.ctrl.maxfaces = 200000 * this.ctrl.more;
         this.ctrl.more = 1;

         selection.add(this.ctrl, 'vislevel', 1, 99, 1)
                     .name('Visibility level')
                     .listen().onChange(() => this.startRedraw(500));
         selection.add(this.ctrl, 'maxnodes', 0, 500000, 1000)
                  .name('Visible nodes')
                  .listen().onChange(() => this.startRedraw(500));
         selection.add(this.ctrl, 'maxfaces', 0, 5000000, 100000)
                  .name('Max faces')
                  .listen().onChange(() => this.startRedraw(500));
      }

      if (this.ctrl.project) {
         const bound = this.getGeomBoundingBox(this.getProjectionSource(), 0.01),
             axis = this.ctrl.project;

         if (this.ctrl.projectPos === undefined)
            this.ctrl.projectPos = (bound.min[axis] + bound.max[axis])/2;

         this._gui.add(this.ctrl, 'projectPos', bound.min[axis], bound.max[axis])
             .name(axis.toUpperCase() + ' projection')
             .onChange(() => this.startDrawGeometry());
      } else {
         // Clipping Options

         const clipFolder = this._gui.addFolder('Clipping');

         for (let naxis = 0; naxis < 3; ++naxis) {
            const cc = this.ctrl.clip[naxis],
                axisC = cc.name.toUpperCase();

            clipFolder.add(cc, 'enabled')
                .name('Enable ' + axisC)
                .listen().onChange(() => this.changedClipping(-1));

            clipFolder.add(cc, 'value', cc.min, cc.max, cc.step)
                .name(axisC + ' position')
                .listen().onChange(() => this.changedClipping(naxis));
         }

         clipFolder.add(this.ctrl, 'clipIntersect').name('Clip intersection')
                   .onChange(() => this.changedClipping(-1));

         clipFolder.add(this.ctrl, 'clipVisualize').name('Visualize')
                   .onChange(() => this.changedClipping(-1));
      }

      // Scene Options

      const scene = this._gui.addFolder('Scene');

      scene.add(this.ctrl.light, 'kind', makeLil(this.ctrl.lightKindItems)).name('Light')
           .listen().onChange(() => {
              light_pnts.show(this.ctrl.light.kind === 'mix' || this.ctrl.light.kind === 'points');
              this.changedLight();
           });

      this.ctrl.light._pnts = this.ctrl.light.specular ? 0 : (this.ctrl.light.front ? 1 : 2);
      const light_pnts = scene.add(this.ctrl.light, '_pnts', { specular: 0, front: 1, box: 2 })
                .name('Positions')
                .show(this.ctrl.light.kind === 'mix' || this.ctrl.light.kind === 'points')
                .onChange(v => {
                   this.ctrl.light.specular = (v === 0);
                   this.ctrl.light.front = (v === 1);
                   this.ctrl.light.top = this.ctrl.light.bottom = this.ctrl.light.left = this.ctrl.light.right = (v === 2);
                   this.changedLight();
                });

      scene.add(this.ctrl.light, 'power', 0, 10, 0.01).name('Power')
           .listen().onChange(() => this.changedLight());

      scene.add(this.ctrl, 'use_fog').name('Fog')
           .listen().onChange(() => this.changedUseFog());


      // Appearance Options

      const appearance = this._gui.addFolder('Appearance');

      this.ctrl._highlight = !this.ctrl.highlight ? 0 : this.ctrl.highlight_bloom ? 2 : 1;
      appearance.add(this.ctrl, '_highlight', { none: 0, normal: 1, bloom: 2 }).name('Highlight Selection')
                .listen().onChange(() => {
                   this.changedHighlight(this.ctrl._highlight);
                   strength.show(this.ctrl._highlight === 2);
                   hcolor.show(this.ctrl._highlight === 1);
                });

      const hcolor = appearance.addColor(this.ctrl, 'highlight_color').name('Hightlight color')
                         .show(this.ctrl._highlight === 1),
            strength = appearance.add(this.ctrl, 'bloom_strength', 0, 3).name('Bloom strength')
                           .listen().onChange(() => this.changedHighlight())
                           .show(this.ctrl._highlight === 2);

      appearance.addColor(this.ctrl, 'background').name('Background')
                .onChange(col => this.changedBackground(col));

      appearance.add(this.ctrl, '_axis', { none: 0, side: 1, center: 2 }).name('Axes')
                    .onChange(() => this.changedAxes());

      if (!this.ctrl.project) {
         appearance.add(this.ctrl, 'rotate').name('Autorotate')
                      .listen().onChange(() => this.changedAutoRotate());
      }

      // Material options

      const material = this._gui.addFolder('Material');
      let material_props = [];

      const addMaterialProp = () => {
         material_props.forEach(f => f.destroy());
         material_props = [];

         const props = this.ctrl.getMaterialCfg()?.props;
         if (!props) return;

         props.forEach(prop => {
            const f = material.add(this.ctrl, prop.name, prop.min, prop.max, prop.step).onChange(() => {
               this.changeMaterialProperty(prop.name);
            });
            material_props.push(f);
         });
      };

      material.add(this.ctrl, 'material_kind', makeLil(this.ctrl.materialKinds)).name('Kind')
              .listen().onChange(() => {
            addMaterialProp();
            this.ensureBloom(false);
            this.changedMaterial();
            this.changedHighlight(); // for some materials bloom will not work
      });

      material.add(this.ctrl, 'transparency', 0, 1, 0.001).name('Transparency')
              .listen().onChange(value => this.changedGlobalTransparency(value));

      material.add(this.ctrl, 'wireframe').name('Wireframe')
              .listen().onChange(() => this.changedWireFrame());

      material.add(this, 'showMaterialDocu').name('Docu from threejs.org');

      addMaterialProp();


      // Camera options
      const camera = this._gui.addFolder('Camera');

      camera.add(this.ctrl, 'camera_kind', makeLil(this.ctrl.cameraKindItems))
            .name('Kind').listen().onChange(() => {
            overlay.show(this.ctrl.camera_kind.indexOf('ortho') === 0);
            this.changeCamera();
      });

      camera.add(this.ctrl, 'can_rotate').name('Can rotate')
                .listen().onChange(() => this.changeCanRotate());

      camera.add(this, 'focusCamera').name('Reset position');

      const overlay = camera.add(this.ctrl, 'camera_overlay', makeLil(this.ctrl.cameraOverlayItems))
                      .name('Overlay').listen().onChange(() => this.changeCamera())
                      .show(this.ctrl.camera_kind.indexOf('ortho') === 0);

      // Advanced Options
      if (this._webgl) {
         const advanced = this._gui.addFolder('Advanced');

         advanced.add(this.ctrl, 'depthTest').name('Depth test')
            .listen().onChange(() => this.changedDepthTest());

         advanced.add(this.ctrl, 'depthMethod', makeLil(this.ctrl.depthMethodItems))
             .name('Rendering order')
             .onChange(method => this.changedDepthMethod(method));

         advanced.add(this, 'resetAdvanced').name('Reset');
      }

      // Transformation Options
      if (!this.ctrl.project) {
         const transform = this._gui.addFolder('Transform');
         transform.add(this.ctrl, 'trans_z', 0.0, 3.0, 0.01)
                     .name('Z axis')
                     .listen().onChange(() => this.changedTransformation());
         transform.add(this.ctrl, 'trans_radial', 0.0, 3.0, 0.01)
                  .name('Radial')
                  .listen().onChange(() => this.changedTransformation());

         transform.add(this, 'resetTransformation').name('Reset');

         if (this.ctrl.trans_z || this.ctrl.trans_radial) transform.open();
      }
   }

   /** @summary show material docu */
   showMaterialDocu() {
      const cfg = this.ctrl.getMaterialCfg();
      if (cfg?.name && typeof window !== 'undefined')
         window.open('https://threejs.org/docs/index.html#api/en/materials/' + cfg.name, '_blank');
   }

   /** @summary Should be called when configuration of highlight is changed */
   changedHighlight(arg) {
      if (arg !== undefined) {
         this.ctrl.highlight = arg !== 0;
         if (this.ctrl.highlight)
            this.ctrl.highlight_bloom = (arg === 2);
      }

      this.ensureBloom();

      if (!this.ctrl.highlight)
         this.highlightMesh(null);

      this._slave_painters?.forEach(p => {
         p.ctrl.highlight = this.ctrl.highlight;
         p.ctrl.highlight_bloom = this.ctrl.highlight_bloom;
         p.ctrl.bloom_strength = this.ctrl.bloom_strength;
         p.changedHighlight();
      });
   }

   /** @summary Handle change of can rotate */
   changeCanRotate(on) {
      if (on !== undefined)
         this.ctrl.can_rotate = on;
      if (this._controls)
         this._controls.enableRotate = this.ctrl.can_rotate;
   }

   /** @summary Change use fog property */
   changedUseFog() {
      this._scene.fog = this.ctrl.use_fog ? this._fog : null;

      this.render3D();
   }

   /** @summary Handle change of camera kind */
   changeCamera() {
      // force control recreation
      if (this._controls) {
          this._controls.cleanup();
          delete this._controls;
      }

      this.ensureBloom(false);

      // recreate camera
      this.createCamera();

      this.createSpecialEffects();

      // set proper position
      this.adjustCameraPosition(true);

      // this.drawOverlay();

      this.addOrbitControls();

      this.render3D();

      // delete this._scene_size; // ensure reassign of camera position

      // this._first_drawing = true;
      // this.startDrawGeometry(true);
   }

   /** @summary create bloom effect */
   ensureBloom(on) {
      if (on === undefined) {
         if (this.ctrl.highlight_bloom === 0)
             this.ctrl.highlight_bloom = this._webgl;

         on = this.ctrl.highlight_bloom && this.ctrl.getMaterialCfg()?.emissive;
      }

      if (on && !this._bloomComposer) {
         this._camera.layers.enable(_BLOOM_SCENE);
         this._bloomComposer = new EffectComposer(this._renderer);
         this._bloomComposer.addPass(new RenderPass(this._scene, this._camera));
         const pass = new UnrealBloomPass(new Vector2(this._scene_width, this._scene_height), 1.5, 0.4, 0.85);
         pass.threshold = 0;
         pass.radius = 0;
         pass.renderToScreen = true;
         this._bloomComposer.addPass(pass);
         this._renderer.autoClear = false;
      } else if (!on && this._bloomComposer) {
         this._bloomComposer.dispose();
         delete this._bloomComposer;
         if (this._renderer)
            this._renderer.autoClear = true;
         this._camera?.layers.disable(_BLOOM_SCENE);
         this._camera?.layers.set(_ENTIRE_SCENE);
      }

      if (this._bloomComposer?.passes)
         this._bloomComposer.passes[1].strength = this.ctrl.bloom_strength;
   }


   /** @summary Show context menu for orbit control
     * @private */
   orbitContext(evnt, intersects) {
      createMenu(evnt, this).then(menu => {
         let numitems = 0, numnodes = 0, cnt = 0;
         if (intersects) {
            for (let n = 0; n < intersects.length; ++n) {
               if (getIntersectStack(intersects[n])) numnodes++;
               if (intersects[n].geo_name) numitems++;
            }
         }

         if (numnodes + numitems === 0)
            this.fillContextMenu(menu);
          else {
            const many = (numnodes + numitems) > 1;

            if (many) menu.add('header:' + ((numitems > 0) ? 'Items' : 'Nodes'));

            for (let n = 0; n < intersects.length; ++n) {
               const obj = intersects[n].object,
                     stack = getIntersectStack(intersects[n]);
               let name, itemname, hdr;

               if (obj.geo_name) {
                  itemname = obj.geo_name;
                  if (itemname.indexOf('<prnt>') === 0)
                     itemname = (this.getItemName() || 'top') + itemname.slice(6);
                  name = itemname.slice(itemname.lastIndexOf('/')+1);
                  if (!name) name = itemname;
                  hdr = name;
               } else if (stack) {
                  name = this._clones.getStackName(stack);
                  itemname = this.getStackFullName(stack);
                  hdr = this.getItemName();
                  if (name.indexOf('Nodes/') === 0)
                     hdr = name.slice(6);
                  else if (name)
                     hdr = name;
                  else if (!hdr)
                     hdr = 'header';
               } else
                  continue;


               cnt++;

               menu.add((many ? 'sub:' : 'header:') + hdr, itemname, arg => this.activateInBrowser([arg], true));

               menu.add('Browse', itemname, arg => this.activateInBrowser([arg], true));

               if (this._hpainter)
                  menu.add('Inspect', itemname, arg => this._hpainter.display(arg, kInspect));

               if (isFunc(this.hidePhysicalNode)) {
                  menu.add('Hide', itemname, arg => this.hidePhysicalNode([arg]));
                  if (cnt > 1) {
                     menu.add('Hide all before', n, indx => {
                        const items = [];
                        for (let i = 0; i < indx; ++i) {
                           const stack = getIntersectStack(intersects[i]);
                           if (stack) items.push(this.getStackFullName(stack));
                        }
                        this.hidePhysicalNode(items);
                     });
                  }
               } else if (obj.geo_name) {
                  menu.add('Hide', n, indx => {
                     const mesh = intersects[indx].object;
                     mesh.visible = false; // just disable mesh
                     if (mesh.geo_object) mesh.geo_object.$hidden_via_menu = true; // and hide object for further redraw
                     menu.painter.render3D();
                  }, 'Hide this physical node');

                  if (many) menu.add('endsub:');

                  continue;
               }

               const wireframe = this.accessObjectWireFrame(obj);
               if (wireframe !== undefined) {
                  menu.addchk(wireframe, 'Wireframe', n, indx => {
                     const m = intersects[indx].object.material;
                     m.wireframe = !m.wireframe;
                     this.render3D();
                  }, 'Toggle wireframe mode for the node');
               }

               if (cnt > 1) {
                  menu.add('Manifest', n, indx => {
                     if (this._last_manifest)
                        this._last_manifest.wireframe = !this._last_manifest.wireframe;

                     if (this._last_hidden)
                        this._last_hidden.forEach(obj => { obj.visible = true; });

                     this._last_hidden = [];

                     for (let i = 0; i < indx; ++i)
                        this._last_hidden.push(intersects[i].object);

                     this._last_hidden.forEach(obj => { obj.visible = false; });

                     this._last_manifest = intersects[indx].object.material;

                     this._last_manifest.wireframe = !this._last_manifest.wireframe;

                     this.render3D();
                  }, 'Manifest selected node');
               }

               menu.add('Focus', n, indx => {
                  this.focusCamera(intersects[indx].object);
               });

               if (!this._geom_viewer) {
                  menu.add('Hide', n, indx => {
                     const resolve = this._clones.resolveStack(intersects[indx].object.stack);
                     if (resolve.obj && (resolve.node.kind === kindGeo) && resolve.obj.fVolume) {
                        setGeoBit(resolve.obj.fVolume, geoBITS.kVisThis, false);
                        updateBrowserIcons(resolve.obj.fVolume, this._hpainter);
                     } else if (resolve.obj && (resolve.node.kind === kindEve)) {
                        resolve.obj.fRnrSelf = false;
                        updateBrowserIcons(resolve.obj, this._hpainter);
                     }

                     this.testGeomChanges();// while many volumes may disappear, recheck all of them
                  }, 'Hide all logical nodes of that kind');
                  menu.add('Hide only this', n, indx => {
                     this._clones.setPhysNodeVisibility(getIntersectStack(intersects[indx]), false);
                     this.testGeomChanges();
                  }, 'Hide only this physical node');
                  if (n > 1) {
                    menu.add('Hide all before', n, indx => {
                        for (let k = 0; k < indx; ++k)
                           this._clones.setPhysNodeVisibility(getIntersectStack(intersects[k]), false);
                        this.testGeomChanges();
                     }, 'Hide all physical nodes before that');
                  }
               }

               if (many) menu.add('endsub:');
            }
         }
         menu.show();
      });
   }

   /** @summary Filter some objects from three.js intersects array */
   filterIntersects(intersects) {
      if (!intersects?.length)
         return intersects;

      // check redirections
      for (let n = 0; n < intersects.length; ++n) {
         if (intersects[n].object.geo_highlight)
            intersects[n].object = intersects[n].object.geo_highlight;
      }

      // remove all elements without stack - indicator that this is geometry object
      // also remove all objects which are mostly transparent
      for (let n = intersects.length - 1; n >= 0; --n) {
         const obj = intersects[n].object;
         let unique = obj.visible && (getIntersectStack(intersects[n]) || (obj.geo_name !== undefined));

         if (unique && obj.material && (obj.material.opacity !== undefined))
            unique = (obj.material.opacity >= 0.1);

         if (obj.jsroot_special) unique = false;

         for (let k = 0; (k < n) && unique; ++k) {
            if (intersects[k].object === obj)
               unique = false;
         }

         if (!unique) intersects.splice(n, 1);
      }

      const clip = this.ctrl.clip;

      if (clip[0].enabled || clip[1].enabled || clip[2].enabled) {
         const clippedIntersects = [];

         for (let i = 0; i < intersects.length; ++i) {
            const point = intersects[i].point, special = (intersects[i].object.type === 'Points');
            let clipped = true;

            if (clip[0].enabled && ((this._clipPlanes[0].normal.dot(point) > this._clipPlanes[0].constant) ^ special)) clipped = false;
            if (clip[1].enabled && ((this._clipPlanes[1].normal.dot(point) > this._clipPlanes[1].constant) ^ special)) clipped = false;
            if (clip[2].enabled && (this._clipPlanes[2].normal.dot(point) > this._clipPlanes[2].constant)) clipped = false;

            if (!clipped) clippedIntersects.push(intersects[i]);
         }

         intersects = clippedIntersects;
      }

      return intersects;
   }

   /** @summary test camera position
     * @desc function analyzes camera position and start redraw of geometry
     *  if objects in view may be changed */
   testCameraPositionChange() {
      if (!this.ctrl.select_in_view || this._draw_all_nodes) return;

      const matrix = createProjectionMatrix(this._camera),
          frustum = createFrustum(matrix);

      // check if overall bounding box seen
      if (!frustum.CheckBox(this.getGeomBoundingBox()))
         this.startDrawGeometry();
   }

   /** @summary Resolve stack */
   resolveStack(stack) {
      return this._clones && stack ? this._clones.resolveStack(stack) : null;
   }

   /** @summary Returns stack full name
     * @desc Includes item name of top geo object */
   getStackFullName(stack) {
      const mainitemname = this.getItemName(),
          sub = this.resolveStack(stack);
      if (!sub || !sub.name)
         return mainitemname;
      return mainitemname ? mainitemname + '/' + sub.name : sub.name;
   }

   /** @summary Add handler which will be called when element is highlighted in geometry drawing
     * @desc Handler should have highlightMesh function with same arguments as TGeoPainter  */
   addHighlightHandler(handler) {
      if (!isFunc(handler?.highlightMesh)) return;
      if (!this._highlight_handlers)
         this._highlight_handlers = [];
      this._highlight_handlers.push(handler);
   }

   /** @summary perform mesh highlight */
   highlightMesh(active_mesh, color, geo_object, geo_index, geo_stack, no_recursive) {
      if (geo_object) {
         active_mesh = active_mesh ? [active_mesh] : [];
         const extras = this.getExtrasContainer();
         if (extras) {
            extras.traverse(obj3d => {
               if ((obj3d.geo_object === geo_object) && (active_mesh.indexOf(obj3d) < 0)) active_mesh.push(obj3d);
            });
         }
      } else if (geo_stack && this._toplevel) {
         active_mesh = [];
         this._toplevel.traverse(mesh => {
            if ((mesh instanceof Mesh) && isSameStack(mesh.stack, geo_stack)) active_mesh.push(mesh);
         });
      } else
         active_mesh = active_mesh ? [active_mesh] : [];

      if (!active_mesh.length)
         active_mesh = null;

      if (active_mesh) {
         // check if highlight is disabled for correspondent objects kinds
         if (active_mesh[0].geo_object) {
            if (!this.ctrl.highlight_scene) active_mesh = null;
         } else
            if (!this.ctrl.highlight) active_mesh = null;
      }

      if (!no_recursive) {
         // check all other painters

         if (active_mesh) {
            if (!geo_object) geo_object = active_mesh[0].geo_object;
            if (!geo_stack) geo_stack = active_mesh[0].stack;
         }

         const lst = this._highlight_handlers || (!this._main_painter ? this._slave_painters : this._main_painter._slave_painters.concat([this._main_painter]));

         for (let k = 0; k < lst?.length; ++k) {
            if (lst[k] !== this)
               lst[k].highlightMesh(null, color, geo_object, geo_index, geo_stack, true);
         }
      }

      const curr_mesh = this._selected_mesh;

      if (!curr_mesh && !active_mesh) return false;

      const get_ctrl = mesh => mesh.get_ctrl ? mesh.get_ctrl() : new GeoDrawingControl(mesh, this.ctrl.highlight_bloom && this._bloomComposer);

      let same = false;

      // check if selections are the same
      if (curr_mesh && active_mesh && (curr_mesh.length === active_mesh.length)) {
         same = true;
         for (let k = 0; (k < curr_mesh.length) && same; ++k)
            if ((curr_mesh[k] !== active_mesh[k]) || get_ctrl(curr_mesh[k]).checkHighlightIndex(geo_index)) same = false;
      }
      if (same) return !!curr_mesh;

      if (curr_mesh) {
         for (let k = 0; k < curr_mesh.length; ++k)
            get_ctrl(curr_mesh[k]).setHighlight();
      }

      this._selected_mesh = active_mesh;

      if (active_mesh) {
         for (let k = 0; k < active_mesh.length; ++k)
            get_ctrl(active_mesh[k]).setHighlight(color || new Color(this.ctrl.highlight_color), geo_index);
      }

      this.render3D(0);

      return !!active_mesh;
   }

   /** @summary handle mouse click event */
   processMouseClick(pnt, intersects, evnt) {
      if (!intersects.length) return;

      const mesh = intersects[0].object;
      if (!mesh.get_ctrl) return;

      const ctrl = mesh.get_ctrl(),
          click_indx = ctrl.extractIndex(intersects[0]);

      ctrl.evnt = evnt;

      if (ctrl.setSelected('blue', click_indx))
         this.render3D();

      ctrl.evnt = null;
   }

   /** @summary Configure mouse delay, required for complex geometries */
   setMouseTmout(val) {
      if (this.ctrl)
         this.ctrl.mouse_tmout = val;

      if (this._controls)
         this._controls.mouse_tmout = val;
   }

   /** @summary Configure depth method, used for render order production.
     * @param {string} method - Allowed values: 'ray', 'box','pnt', 'size', 'dflt' */
   setDepthMethod(method) {
      if (this.ctrl)
         this.ctrl.depthMethod = method;
   }

   /** @summary Returns if camera can rotated */
   canRotateCamera() {
      if (this.ctrl.can_rotate === false)
         return false;
      if (!this.ctrl.can_rotate && (this.isOrthoCamera() || this.ctrl.project))
         return false;
      return true;
   }

   /** @summary Add orbit control */
   addOrbitControls() {
      if (this._controls || !this._webgl || this.isBatchMode() || this.superimpose) return;

      if (!this.getCanvPainter())
         this.setTooltipAllowed(settings.Tooltip);

      this._controls = createOrbitControl(this, this._camera, this._scene, this._renderer, this._lookat);

      this._controls.mouse_tmout = this.ctrl.mouse_tmout; // set larger timeout for geometry processing

      if (!this.canRotateCamera())
         this._controls.enableRotate = false;

      this._controls.contextMenu = this.orbitContext.bind(this);

      this._controls.processMouseMove = intersects => {
         // painter already cleaned up, ignore any incoming events
         if (!this.ctrl || !this._controls) return;

         let active_mesh = null, tooltip = null, resolve = null, names = [], geo_object, geo_index, geo_stack;

         // try to find mesh from intersections
         for (let k = 0; k < intersects.length; ++k) {
            const obj = intersects[k].object, stack = getIntersectStack(intersects[k]);
            if (!obj || !obj.visible) continue;
            let info = null;
            if (obj.geo_object)
               info = obj.geo_name;
            else if (stack)
               info = this.getStackFullName(stack);
            if (!info) continue;

            if (info.indexOf('<prnt>') === 0)
               info = this.getItemName() + info.slice(6);

            names.push(info);

            if (!active_mesh) {
               active_mesh = obj;
               tooltip = info;
               geo_object = obj.geo_object;
               if (obj.get_ctrl) {
                  geo_index = obj.get_ctrl().extractIndex(intersects[k]);
                  if ((geo_index !== undefined) && isStr(tooltip))
                     tooltip += ' indx:' + JSON.stringify(geo_index);
               }
               geo_stack = stack;

               if (geo_stack) {
                  resolve = this.resolveStack(geo_stack);
                  if (obj.stacks) geo_index = intersects[k].instanceId;
               }
            }
         }

         this.highlightMesh(active_mesh, undefined, geo_object, geo_index);

         if (this.ctrl.update_browser) {
            if (this.ctrl.highlight && tooltip) names = [tooltip];
            this.activateInBrowser(names);
         }

         if (!resolve?.obj)
            return tooltip;

         const lines = provideObjectInfo(resolve.obj);
         lines.unshift(tooltip);

         return { name: resolve.obj.fName, title: resolve.obj.fTitle || resolve.obj._typename, lines };
      };

      this._controls.processMouseLeave = function() {
         this.processMouseMove([]); // to disable highlight and reset browser
      };

      this._controls.processDblClick = () => {
         // painter already cleaned up, ignore any incoming events
         if (!this.ctrl || !this._controls) return;

         if (this._last_manifest) {
            this._last_manifest.wireframe = !this._last_manifest.wireframe;
            if (this._last_hidden)
               this._last_hidden.forEach(obj => { obj.visible = true; });
            delete this._last_hidden;
            delete this._last_manifest;
         } else
            this.adjustCameraPosition(true);

         this.render3D();
      };
   }

   /** @summary Main function in geometry creation loop
     * @desc Returns:
     * - false when nothing todo
     * - true if one could perform next action immediately
     * - 1 when call after short timeout required
     * - 2 when call must be done from processWorkerReply */
   nextDrawAction() {
      if (!this._clones || this.isStage(stageInit)) return false;

      if (this.isStage(stageCollect)) {
         if (this._geom_viewer) {
            this._draw_all_nodes = false;
            this.changeStage(stageAnalyze);
            return true;
         }

         // wait until worker is really started
         if (this.ctrl.use_worker > 0) {
            if (!this._worker) { this.startWorker(); return 1; }
            if (!this._worker_ready) return 1;
         }

         // first copy visibility flags and check how many unique visible nodes exists
         let numvis = this._first_drawing ? this._clones.countVisibles() : 0,
             matrix = null, frustum = null;

         if (!numvis)
            numvis = this._clones.markVisibles(false, false, !!this.geo_manager && !this.ctrl.showtop);

         if (this.ctrl.select_in_view && !this._first_drawing) {
            // extract camera projection matrix for selection

            matrix = createProjectionMatrix(this._camera);

            frustum = createFrustum(matrix);

            // check if overall bounding box seen
            if (frustum.CheckBox(this.getGeomBoundingBox())) {
               matrix = null; // not use camera for the moment
               frustum = null;
            }
         }

         this._current_face_limit = this.ctrl.maxfaces;
         if (matrix) this._current_face_limit *= 1.25;

         // here we decide if we need worker for the drawings
         // main reason - too large geometry and large time to scan all camera positions
         let need_worker = !this.isBatchMode() && browser.isChrome && ((numvis > 10000) || (matrix && (this._clones.scanVisible() > 1e5)));

         // worker does not work when starting from file system
         if (need_worker && source_dir.indexOf('file://') === 0) {
            console.log('disable worker for jsroot from file system');
            need_worker = false;
         }

         if (need_worker && !this._worker && (this.ctrl.use_worker >= 0))
            this.startWorker(); // we starting worker, but it may not be ready so fast

         if (!need_worker || !this._worker_ready) {
            const res = this._clones.collectVisibles(this._current_face_limit, frustum);
            this._new_draw_nodes = res.lst;
            this._draw_all_nodes = res.complete;
            this.changeStage(stageAnalyze);
            return true;
         }

         const job = {
            collect: this._current_face_limit,   // indicator for the command
            flags: this._clones.getVisibleFlags(),
            matrix: matrix ? matrix.elements : null,
            vislevel: this._clones.getVisLevel(),
            maxvisnodes: this._clones.getMaxVisNodes()
         };

         this.submitToWorker(job);

         this.changeStage(stageWorkerCollect);

         return 2; // we now waiting for the worker reply
      }

      if (this.isStage(stageWorkerCollect)) {
         // do nothing, we are waiting for worker reply
         return 2;
      }

      if (this.isStage(stageAnalyze)) {
         // here we merge new and old list of nodes for drawing,
         // normally operation is fast and can be implemented with one c

         if (this._new_append_nodes) {
            this._new_draw_nodes = this._draw_nodes.concat(this._new_append_nodes);

            delete this._new_append_nodes;
         } else if (this._draw_nodes) {
            let del;
            if (this._geom_viewer)
               del = this._draw_nodes;
            else
               del = this._clones.mergeVisibles(this._new_draw_nodes, this._draw_nodes);

            // remove should be fast, do it here
            for (let n = 0; n < del.length; ++n)
               this._clones.createObject3D(del[n].stack, this._toplevel, 'delete_mesh');

            if (del.length > 0)
               this.drawing_log = `Delete ${del.length} nodes`;
         }

         this._draw_nodes = this._new_draw_nodes;
         delete this._new_draw_nodes;
         this.changeStage(stageCollShapes);
         return true;
      }

      if (this.isStage(stageCollShapes)) {
         // collect shapes
         const shapes = this._clones.collectShapes(this._draw_nodes);

         // merge old and new list with produced shapes
         this._build_shapes = this._clones.mergeShapesLists(this._build_shapes, shapes);

         this.changeStage(stageStartBuild);
         return true;
      }

      if (this.isStage(stageStartBuild)) {
         // this is building of geometries,
         // one can ask worker to build them or do it ourself

         if (this.canSubmitToWorker()) {
            const job = { limit: this._current_face_limit, shapes: [] };
            let cnt = 0;
            for (let n = 0; n < this._build_shapes.length; ++n) {
               let cl = null;
               const item = this._build_shapes[n];
               // only submit not-done items
               if (item.ready || item.geom) {
                  // this is place holder for existing geometry
                  cl = { id: item.id, ready: true, nfaces: countGeometryFaces(item.geom), refcnt: item.refcnt };
               } else {
                  cl = clone(item, null, true);
                  cnt++;
               }

               job.shapes.push(cl);
            }

            if (cnt > 0) {
               /// only if some geom missing, submit job to the worker
               this.submitToWorker(job);
               this.changeStage(stageWorkerBuild);
               return 2;
            }
         }

         this.changeStage(stageBuild);
      }

      if (this.isStage(stageWorkerBuild)) {
         // waiting shapes from the worker, worker should activate our code
         return 2;
      }

      if (this.isStage(stageBuild) || this.isStage(stageBuildReady)) {
         if (this.isStage(stageBuild)) {
            // building shapes

            const res = this._clones.buildShapes(this._build_shapes, this._current_face_limit, 500);
            if (res.done) {
               this.ctrl.info.num_shapes = this._build_shapes.length;
               this.changeStage(stageBuildReady);
            } else {
               this.ctrl.info.num_shapes = res.shapes;
               this.drawing_log = `Creating: ${res.shapes} / ${this._build_shapes.length} shapes,  ${res.faces} faces`;
               return true;
               // if (res.notusedshapes < 30) return true;
            }
         }

         // final stage, create all meshes

         const tm0 = new Date().getTime(),
               toplevel = this.ctrl.project ? this._full_geom : this._toplevel;
         let build_instanced = false, ready = true;

         if (!this.ctrl.project)
            build_instanced = this._clones.createInstancedMeshes(this.ctrl, toplevel, this._draw_nodes, this._build_shapes, getRootColors());

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

               /// shape can be provided with entry itself
               const shape = entry.server_shape || this._build_shapes[entry.shapeid];

               this.createEntryMesh(entry, shape, toplevel);

               const tm1 = new Date().getTime();
               if (tm1 - tm0 > 500) { ready = false; break; }
            }
         }

         if (ready) {
            if (this.ctrl.project) {
               this.changeStage(stageBuildProj);
               return true;
            }
            this.changeStage(stageInit);
            return false;
         }

         if (!this.isStage(stageBuild))
            this.drawing_log = `Building meshes ${this.ctrl.info.num_meshes} / ${this.ctrl.info.num_faces}`;
         return true;
      }

      if (this.isStage(stageWaitMain)) {
         // wait for main painter to be ready

         if (!this._main_painter) {
            this.changeStage(stageInit, 'Lost main painter');
            return false;
         }
         if (!this._main_painter._drawing_ready) return 1;

         this.changeStage(stageBuildProj); // just do projection
      }

      if (this.isStage(stageBuildProj)) {
         this.doProjection();
         this.changeStage(stageInit);
         return false;
      }

      console.error(`never come here, stage ${this.drawing_stage}`);

      return false;
   }

   /** @summary Insert appropriate mesh for given entry */
   createEntryMesh(entry, shape, toplevel) {
      // workaround for the TGeoOverlap, where two branches should get predefined color
      if (this._splitColors && entry.stack) {
         if (entry.stack[0] === 0)
            entry.custom_color = 'green';
         else if (entry.stack[0] === 1)
            entry.custom_color = 'blue';
      }

      this._clones.createEntryMesh(this.ctrl, toplevel, entry, shape, getRootColors());

      return true;
   }

   /** @summary used by geometry viewer to show more nodes
     * @desc These nodes excluded from selection logic and always inserted into the model
     * Shape already should be created and assigned to the node */
   appendMoreNodes(nodes, from_drawing) {
      if (!this.isStage(stageInit) && !from_drawing) {
         this._provided_more_nodes = nodes;
         return;
      }

      // delete old nodes
      if (this._more_nodes) {
         for (let n = 0; n < this._more_nodes.length; ++n) {
            const entry = this._more_nodes[n],
                obj3d = this._clones.createObject3D(entry.stack, this._toplevel, 'delete_mesh');
            disposeThreejsObject(obj3d);
            cleanupShape(entry.server_shape);
            delete entry.server_shape;
         }
      }

      delete this._more_nodes;

      if (!nodes) return;

      const real_nodes = [];

      for (let k = 0; k < nodes.length; ++k) {
         const entry = nodes[k],
             shape = entry.server_shape;
         if (!shape?.ready) continue;

         if (this.createEntryMesh(entry, shape, this._toplevel))
            real_nodes.push(entry);
      }

      // remember additional nodes only if they include shape - otherwise one can ignore them
      if (real_nodes.length > 0)
         this._more_nodes = real_nodes;

      if (!from_drawing) this.render3D();
   }

   /** @summary Returns hierarchy of 3D objects used to produce projection.
     * @desc Typically external master painter is used, but also internal data can be used */
   getProjectionSource() {
      if (this._clones_owner)
         return this._full_geom;
      if (!this._main_painter) {
         console.warn('MAIN PAINTER DISAPPER');
         return null;
      }
      if (!this._main_painter._drawing_ready) {
         console.warn('MAIN PAINTER NOT READY WHEN DO PROJECTION');
         return null;
      }
      return this._main_painter._toplevel;
   }

   /** @summary Extend custom geometry bounding box */
   extendCustomBoundingBox(box) {
      if (!box) return;
      if (!this._customBoundingBox)
         this._customBoundingBox = new Box3().makeEmpty();

      const origin = this._customBoundingBox.clone();
      this._customBoundingBox.union(box);

      if (!this._customBoundingBox.equals(origin))
         this._adjust_camera_with_render = true;
   }

   /** @summary Calculate geometry bounding box */
   getGeomBoundingBox(topitem, scalar) {
      const box3 = new Box3(), check_any = !this._clones;
      if (topitem === undefined)
         topitem = this._toplevel;

      box3.makeEmpty();

      if (this._customBoundingBox && (topitem === this._toplevel)) {
         box3.union(this._customBoundingBox);
         return box3;
      }

      if (!topitem) {
         box3.min.x = box3.min.y = box3.min.z = -1;
         box3.max.x = box3.max.y = box3.max.z = 1;
         return box3;
      }

      topitem.traverse(mesh => {
         if (check_any || (mesh.stack && (mesh instanceof Mesh)) ||
             (mesh.main_track && (mesh instanceof LineSegments)) || (mesh.stacks && (mesh instanceof InstancedMesh)))
            getBoundingBox(mesh, box3);
      });

      if (scalar === 'original') {
         box3.translate(new Vector3(-topitem.position.x, -topitem.position.y, -topitem.position.z));
         box3.min.multiply(new Vector3(1/topitem.scale.x, 1/topitem.scale.y, 1/topitem.scale.z));
         box3.max.multiply(new Vector3(1/topitem.scale.x, 1/topitem.scale.y, 1/topitem.scale.z));
      } else if (scalar !== undefined)
         box3.expandByVector(box3.getSize(new Vector3()).multiplyScalar(scalar));

      return box3;
   }

   /** @summary Create geometry projection */
   doProjection() {
      const toplevel = this.getProjectionSource();

      if (!toplevel) return false;

      disposeThreejsObject(this._toplevel, true);

      // let axis = this.ctrl.project;

      if (this.ctrl.projectPos === undefined) {
         const bound = this.getGeomBoundingBox(toplevel),
               min = bound.min[this.ctrl.project], max = bound.max[this.ctrl.project];
         let mean = (min + max)/2;

         if ((min < 0) && (max > 0) && (Math.abs(mean) < 0.2*Math.max(-min, max))) mean = 0; // if middle is around 0, use 0

         this.ctrl.projectPos = mean;
      }

      toplevel.traverse(mesh => {
         if (!(mesh instanceof Mesh) || !mesh.stack) return;

         const geom2 = projectGeometry(mesh.geometry, mesh.parent.absMatrix || mesh.parent.matrixWorld, this.ctrl.project, this.ctrl.projectPos, mesh._flippedMesh);

         if (!geom2) return;

         const mesh2 = new Mesh(geom2, mesh.material.clone());

         this._toplevel.add(mesh2);

         mesh2.stack = mesh.stack;
      });

      return true;
   }

   /** @summary Should be invoked when light configuration changed */
   changedLight(box) {
      if (!this._camera) return;

      const need_render = !box;

      if (!box) box = this.getGeomBoundingBox();

      const sizex = box.max.x - box.min.x,
          sizey = box.max.y - box.min.y,
          sizez = box.max.z - box.min.z,
          plights = [], p = (this.ctrl.light.power ?? 1) * 0.5;

      if (this._camera._lights !== this.ctrl.light.kind) {
         // remove all childs and recreate only necessary lights
         disposeThreejsObject(this._camera, true);

         this._camera._lights = this.ctrl.light.kind;

         switch (this._camera._lights) {
            case 'ambient' : this._camera.add(new AmbientLight(0xefefef, p)); break;
            case 'hemisphere' : this._camera.add(new HemisphereLight(0xffffbb, 0x080820, p)); break;
            case 'mix': this._camera.add(new AmbientLight(0xefefef, p)); // intentionally without break

            // eslint-disable-next-line no-fallthrough
            default: // 6 point lights
               for (let n = 0; n < 6; ++n) {
                  const l = new DirectionalLight(0xefefef, p);
                  this._camera.add(l);
                  l._id = n;
               }
         }
      }

      for (let k = 0; k < this._camera.children.length; ++k) {
         const light = this._camera.children[k];
         let enabled = false;
         if (light.isAmbientLight || light.isHemisphereLight) {
            light.intensity = p;
            continue;
         }
         if (!light.isDirectionalLight) continue;
         switch (light._id) {
            case 0: light.position.set(sizex/5, sizey/5, sizez/5); enabled = this.ctrl.light.specular; break;
            case 1: light.position.set(0, 0, sizez/2); enabled = this.ctrl.light.front; break;
            case 2: light.position.set(0, 2*sizey, 0); enabled = this.ctrl.light.top; break;
            case 3: light.position.set(0, -2*sizey, 0); enabled = this.ctrl.light.bottom; break;
            case 4: light.position.set(-2*sizex, 0, 0); enabled = this.ctrl.light.left; break;
            case 5: light.position.set(2*sizex, 0, 0); enabled = this.ctrl.light.right; break;
         }
         light.power = enabled ? p*Math.PI*4 : 0;
         if (enabled) plights.push(light);
      }

      // keep light power of all soources constant
      plights.forEach(ll => { ll.power = p*4*Math.PI/plights.length; });

      if (need_render) this.render3D();
   }

   /** @summary Returns true if orthogarphic camera is used */
   isOrthoCamera() {
      return this.ctrl.camera_kind.indexOf('ortho') === 0;
   }

   /** @summary Create configured camera */
   createCamera() {
      if (this._camera) {
          this._scene.remove(this._camera);
          disposeThreejsObject(this._camera);
          delete this._camera;
       }

      if (this.isOrthoCamera())
         this._camera = new OrthographicCamera(-this._scene_width/2, this._scene_width/2, this._scene_height/2, -this._scene_height/2, 1, 10000);
       else {
         this._camera = new PerspectiveCamera(25, this._scene_width / this._scene_height, 1, 10000);
         this._camera.up = this.ctrl._yup ? new Vector3(0, 1, 0) : new Vector3(0, 0, 1);
      }

      // Light - add default directional light, adjust later
      const light = new DirectionalLight(0xefefef, 0.1);
      light.position.set(10, 10, 10);
      this._camera.add(light);

      this._scene.add(this._camera);
   }

   /** @summary Create special effects */
   createSpecialEffects() {
      if (this._webgl && this.ctrl.outline && isFunc(this.createOutline)) {
         // code used with jsroot-based geometry drawing in EVE7, not important any longer
         this._effectComposer = new EffectComposer(this._renderer);
         this._effectComposer.addPass(new RenderPass(this._scene, this._camera));
         this.createOutline(this._scene_width, this._scene_height);
      }

      this.ensureBloom();
   }

   /** @summary Initial scene creation */
   async createScene(w, h, render3d) {
      if (this.superimpose) {
         const cfg = getHistPainter3DCfg(this.getMainPainter());

         if (cfg?.renderer) {
            this._scene = cfg.scene;
            this._scene_width = cfg.scene_width;
            this._scene_height = cfg.scene_height;
            this._renderer = cfg.renderer;
            this._webgl = (this._renderer.jsroot_render3d === constants.Render3D.WebGL);

            this._toplevel = new Object3D();
            this._scene.add(this._toplevel);

            if (cfg.scale_x || cfg.scale_y || cfg.scale_z)
               this._toplevel.scale.set(cfg.scale_x, cfg.scale_y, cfg.scale_z);
            if (cfg.offset_x || cfg.offset_y || cfg.offset_z)
               this._toplevel.position.set(cfg.offset_x, cfg.offset_y, cfg.offset_z);
            this._toplevel.updateMatrix();
            this._toplevel.updateMatrixWorld();

            this._camera = cfg.camera;
         }

         return this._renderer?.jsroot_dom;
      }

      // three.js 3D drawing
      this._scene = new Scene();
      this._fog = new Fog(0xffffff, 1, 10000);
      this._scene.fog = this.ctrl.use_fog ? this._fog : null;

      this._scene.overrideMaterial = new MeshLambertMaterial({ color: 0x7000ff, vertexColors: false, transparent: true, opacity: 0.2, depthTest: false });

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

      this.createCamera();

      this._selected_mesh = null;

      this._overall_size = 10;

      this._toplevel = new Object3D();

      this._scene.add(this._toplevel);

      this._scene.background = new Color(this.ctrl.background);

      return createRender3D(w, h, render3d, { antialias: true, logarithmicDepthBuffer: false, preserveDrawingBuffer: true })
        .then(r => {
         this._renderer = r;

         if (this.batch_format)
            r.jsroot_image_format = this.batch_format;

         this._webgl = (this._renderer.jsroot_render3d === constants.Render3D.WebGL);

         if (this._renderer.setPixelRatio && !isNodeJs())
            this._renderer.setPixelRatio(window.devicePixelRatio);
         this._renderer.setSize(w, h, !this._fit_main_area);
         this._renderer.localClippingEnabled = true;

         this._renderer.setClearColor(this._scene.background, 1);

         if (this._fit_main_area && this._webgl) {
            this._renderer.domElement.style.width = '100%';
            this._renderer.domElement.style.height = '100%';
            const main = this.selectDom();
            if (main.style('position') === 'static')
               main.style('position', 'relative');
         }

         this._animating = false;

         this.ctrl.doubleside = false; // both sides need for clipping
         this.createSpecialEffects();

         if (this._fit_main_area && !this._webgl) {
            // create top-most SVG for geomtery drawings
            const doc = getDocument(),
                  svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svg.setAttribute('width', w);
            svg.setAttribute('height', h);
            svg.appendChild(this._renderer.jsroot_dom);
            return svg;
         }

         return this._renderer.jsroot_dom;
      });
   }

   /** @summary Start geometry drawing */
   startDrawGeometry(force) {
      if (!force && !this.isStage(stageInit)) {
         this._draw_nodes_again = true;
         return;
      }

      if (this._clones_owner && this._clones)
         this._clones.setDefaultColors(this.ctrl.dflt_colors);

      this._startm = new Date().getTime();
      this._last_render_tm = this._startm;
      this._last_render_meshes = 0;
      this.changeStage(stageCollect);
      this._drawing_ready = false;
      this.ctrl.info.num_meshes = 0;
      this.ctrl.info.num_faces = 0;
      this.ctrl.info.num_shapes = 0;
      this._selected_mesh = null;

      if (this.ctrl.project) {
         if (this._clones_owner) {
            if (this._full_geom)
               this.changeStage(stageBuildProj);
             else
               this._full_geom = new Object3D();
         } else
            this.changeStage(stageWaitMain);
      }

      delete this._last_manifest;
      delete this._last_hidden; // clear list of hidden objects

      delete this._draw_nodes_again; // forget about such flag

      this.continueDraw();
   }

   /** @summary reset all kind of advanced features like depth test */
   resetAdvanced() {
      this.ctrl.depthTest = true;
      this.ctrl.clipIntersect = true;
      this.ctrl.depthMethod = 'ray';

      this.changedDepthMethod('norender');
      this.changedDepthTest();
   }

   /** @summary returns maximal dimension */
   getOverallSize(force) {
      if (!this._overall_size || force || this._customBoundingBox) {
         const box = this.getGeomBoundingBox();

         // if detect of coordinates fails - ignore
         if (!Number.isFinite(box.min.x)) return 1000;

         this._overall_size = 2 * Math.max(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z);
      }

      return this._overall_size;
   }

   /** @summary Create png image with drawing snapshot. */
   createSnapshot(filename) {
      if (!this._renderer) return;
      this.render3D(0);
      const dataUrl = this._renderer.domElement.toDataURL('image/png');
      if (filename === 'asis') return dataUrl;
      dataUrl.replace('image/png', 'image/octet-stream');
      const doc = getDocument(),
            link = doc.createElement('a');
      if (isStr(link.download)) {
         doc.body.appendChild(link); // Firefox requires the link to be in the body
         link.download = filename || 'geometry.png';
         link.href = dataUrl;
         link.click();
         doc.body.removeChild(link); // remove the link when done
      }
   }

   /** @summary Returns url parameters defining camera position.
     * @desc Either absolute position are provided (arg === true) or zoom, roty, rotz parameters */
   produceCameraUrl(arg) {
      if (!this._camera)
         return '';

      if (this._camera.isOrthographicCamera) {
         const zoom = Math.round(this._camera.zoom * 100);
         return this.ctrl.camera_kind + (zoom === 100 ? '' : `,zoom=${zoom}`);
      }

      let kind = '';
      if (this.ctrl.camera_kind !== 'perspective')
        kind = this.ctrl.camera_kind + ',';

      if (arg === true) {
         const p = this._camera?.position, t = this._controls?.target;
         if (!p || !t) return '';

         const conv = v => {
            let s = '';
            if (v < 0) { s = 'n'; v = -v; }
            return s + v.toFixed(0);
         };

         let res = `${kind}camx${conv(p.x)},camy${conv(p.y)},camz${conv(p.z)}`;
         if (t.x || t.y || t.z) res += `,camlx${conv(t.x)},camly${conv(t.y)},camlz${conv(t.z)}`;
         return res;
      }

      if (!this._lookat || !this._camera0pos)
         return '';

      const pos1 = new Vector3().add(this._camera0pos).sub(this._lookat),
          pos2 = new Vector3().add(this._camera.position).sub(this._lookat),
          zoom = Math.min(10000, Math.max(1, this.ctrl.zoom * pos2.length() / pos1.length() * 100));

      pos1.normalize();
      pos2.normalize();

      const quat = new Quaternion(), euler = new Euler();

      quat.setFromUnitVectors(pos1, pos2);
      euler.setFromQuaternion(quat, 'YZX');

      let roty = euler.y / Math.PI * 180,
          rotz = euler.z / Math.PI * 180;

      if (roty < 0) roty += 360;
      if (rotz < 0) rotz += 360;
      return `${kind}roty${roty.toFixed(0)},rotz${rotz.toFixed(0)},zoom${zoom.toFixed(0)}`;
   }

   /** @summary Calculates current zoom factor */
   calculateZoom() {
      if (this._camera0pos && this._camera && this._lookat) {
         const pos1 = new Vector3().add(this._camera0pos).sub(this._lookat),
             pos2 = new Vector3().add(this._camera.position).sub(this._lookat);
         return pos2.length() / pos1.length();
      }

      return 0;
   }

   /** @summary Place camera to default position,
     * @param arg - true forces camera readjustment, 'first' is called when suppose to be first after complete drawing
     * @param keep_zoom - tries to keep zomming factor of the camera */
   adjustCameraPosition(arg, keep_zoom) {
      if (!this._toplevel || this.superimpose) return;

      const force = (arg === true),
          first_time = (arg === 'first') || force,
          only_set = (arg === 'only_set'),
          box = this.getGeomBoundingBox();

      // let box2 = new Box3().makeEmpty();
      // box2.expandByObject(this._toplevel, true);
      // console.log('min,max', box.min.x, box.max.x, box.min.y, box.max.y, box.min.z, box.max.z );

      // if detect of coordinates fails - ignore
      if (!Number.isFinite(box.min.x)) {
         console.log('FAILS to get geometry bounding box');
         return;
      }

      const 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,
            more = this.ctrl._axis || (this.ctrl.camera_overlay === 'bar') ? 0.2 : 0.1;

      if (this._scene_size && !force) {
         const d = this._scene_size, test = (v1, v2, scale) => {
            if (!scale) scale = Math.abs((v1+v2)/2);
            return scale <= 1e-20 ? true : Math.abs(v2-v1)/scale > 0.01;
         },
          large_change = test(sizex, d.sizex) || test(sizey, d.sizey) || test(sizez, d.sizez) ||
                            test(midx, d.midx, d.sizex) || test(midy, d.midy, d.sizey) || test(midz, d.midz, d.sizez);
         if (!large_change) {
            if (this.ctrl.select_in_view)
               this.startDrawGeometry();
            return;
         }
      }

      this._scene_size = { sizex, sizey, sizez, midx, midy, midz };

      this._overall_size = 2 * Math.max(sizex, sizey, sizez);

      this._camera.near = this._overall_size / 350;
      this._camera.far = this._overall_size * 100;
      this._fog.near = this._overall_size * 0.5;
      this._fog.far = this._overall_size * 5;

      if (first_time) {
         for (let naxis = 0; naxis < 3; ++naxis) {
            const cc = this.ctrl.clip[naxis];
            cc.min = box.min[cc.name];
            cc.max = box.max[cc.name];
            const sz = cc.max - cc.min;
            cc.max += sz*0.01;
            cc.min -= sz*0.01;
            if (sz > 100)
               cc.step = 0.1;
            else if (sz > 1)
               cc.step = 0.001;
            else
               cc.step = undefined;

            if (!cc.value)
               cc.value = (cc.min + cc.max) / 2;
            else if (cc.value < cc.min)
               cc.value = cc.min;
            else if (cc.value > cc.max)
               cc.value = cc.max;
         }
      }

      let k = 2*this.ctrl.zoom;
      const max_all = Math.max(sizex, sizey, sizez),
            sign = this.ctrl.camera_kind.indexOf('N') > 0 ? -1 : 1;

      this._lookat = new Vector3(midx, midy, midz);
      this._camera0pos = new Vector3(-2*max_all, 0, 0); // virtual 0 position, where rotation starts

      this._camera.updateMatrixWorld();
      this._camera.updateProjectionMatrix();

      if ((this.ctrl.rotatey || this.ctrl.rotatez) && this.ctrl.can_rotate) {
         const prev_zoom = this.calculateZoom();
         if (keep_zoom && prev_zoom) k = 2*prev_zoom;

         const euler = new Euler(0, this.ctrl.rotatey/180*Math.PI, this.ctrl.rotatez/180*Math.PI, 'YZX');

         this._camera.position.set(-k*max_all, 0, 0);
         this._camera.position.applyEuler(euler);
         this._camera.position.add(new Vector3(midx, midy, midz));

         if (keep_zoom && prev_zoom) {
            const actual_zoom = this.calculateZoom();
            k *= prev_zoom/actual_zoom;

            this._camera.position.set(-k*max_all, 0, 0);
            this._camera.position.applyEuler(euler);
            this._camera.position.add(new Vector3(midx, midy, midz));
         }
      } else if (this.ctrl.camx !== undefined && this.ctrl.camy !== undefined && this.ctrl.camz !== undefined) {
         this._camera.position.set(this.ctrl.camx, this.ctrl.camy, this.ctrl.camz);
         this._lookat.set(this.ctrl.camlx || 0, this.ctrl.camly || 0, this.ctrl.camlz || 0);
         this.ctrl.camx = this.ctrl.camy = this.ctrl.camz = this.ctrl.camlx = this.ctrl.camly = this.ctrl.camlz = undefined;
      } else if ((this.ctrl.camera_kind === 'orthoXOY') || (this.ctrl.camera_kind === 'orthoXNOY')) {
         this._camera.up.set(0, 1, 0);
         this._camera.position.set(sign < 0 ? midx*2 : 0, 0, midz + sign*sizez*2);
         this._lookat.set(sign < 0 ? midx*2 : 0, 0, midz);
         this._camera.left = box.min.x - more*sizex;
         this._camera.right = box.max.x + more*sizex;
         this._camera.top = box.max.y + more*sizey;
         this._camera.bottom = box.min.y - more*sizey;
         if (!keep_zoom) this._camera.zoom = this.ctrl.zoom || 1;
         this._camera.orthoSign = sign;
         this._camera.orthoZ = [midz, sizez/2];
      } else if ((this.ctrl.camera_kind === 'orthoXOZ') || (this.ctrl.camera_kind === 'orthoXNOZ')) {
         this._camera.up.set(0, 0, 1);
         this._camera.position.set(sign < 0 ? midx*2 : 0, midy - sign*sizey*2, 0);
         this._lookat.set(sign < 0 ? midx*2 : 0, midy, 0);
         this._camera.left = box.min.x - more*sizex;
         this._camera.right = box.max.x + more*sizex;
         this._camera.top = box.max.z + more*sizez;
         this._camera.bottom = box.min.z - more*sizez;
         if (!keep_zoom) this._camera.zoom = this.ctrl.zoom || 1;
         this._camera.orthoIndicies = [0, 2, 1];
         this._camera.orthoRotation = geom => geom.rotateX(Math.PI/2);
         this._camera.orthoSign = sign;
         this._camera.orthoZ = [midy, -sizey/2];
      } else if ((this.ctrl.camera_kind === 'orthoZOY') || (this.ctrl.camera_kind === 'orthoZNOY')) {
         this._camera.up.set(0, 1, 0);
         this._camera.position.set(midx - sign*sizex*2, 0, sign < 0 ? midz*2 : 0);
         this._lookat.set(midx, 0, sign < 0 ? midz*2 : 0);
         this._camera.left = box.min.z - more*sizez;
         this._camera.right = box.max.z + more*sizez;
         this._camera.top = box.max.y + more*sizey;
         this._camera.bottom = box.min.y - more*sizey;
         if (!keep_zoom) this._camera.zoom = this.ctrl.zoom || 1;
         this._camera.orthoIndicies = [2, 1, 0];
         this._camera.orthoRotation = geom => geom.rotateY(-Math.PI/2);
         this._camera.orthoSign = sign;
         this._camera.orthoZ = [midx, -sizex/2];
      } else if ((this.ctrl.camera_kind === 'orthoZOX') || (this.ctrl.camera_kind === 'orthoZNOX')) {
         this._camera.up.set(1, 0, 0);
         this._camera.position.set(0, midy - sign*sizey*2, sign > 0 ? midz*2 : 0);
         this._lookat.set(0, midy, sign > 0 ? midz*2 : 0);
         this._camera.left = box.min.z - more*sizez;
         this._camera.right = box.max.z + more*sizez;
         this._camera.top = box.max.x + more*sizex;
         this._camera.bottom = box.min.x - more*sizex;
         if (!keep_zoom) this._camera.zoom = this.ctrl.zoom || 1;
         this._camera.orthoIndicies = [2, 0, 1];
         this._camera.orthoRotation = geom => geom.rotateX(Math.PI/2).rotateY(Math.PI/2);
         this._camera.orthoSign = sign;
         this._camera.orthoZ = [midy, -sizey/2];
      } else if (this.ctrl.project) {
         switch (this.ctrl.project) {
            case 'x': this._camera.position.set(k*1.5*Math.max(sizey, sizez), 0, 0); break;
            case 'y': this._camera.position.set(0, k*1.5*Math.max(sizex, sizez), 0); break;
            case 'z': this._camera.position.set(0, 0, k*1.5*Math.max(sizex, sizey)); break;
         }
      } else if (this.ctrl.camera_kind === 'perspXOZ') {
         this._camera.up.set(0, 1, 0);
         this._camera.position.set(midx - 3*max_all, midy, midz);
      } else if (this.ctrl.camera_kind === 'perspYOZ') {
         this._camera.up.set(1, 0, 0);
         this._camera.position.set(midx, midy - 3*max_all, midz);
      } else if (this.ctrl.camera_kind === 'perspXOY') {
         this._camera.up.set(0, 0, 1);
         this._camera.position.set(midx - 3*max_all, midy, midz);
      } else if (this.ctrl._yup) {
         this._camera.up.set(0, 1, 0);
         this._camera.position.set(midx-k*Math.max(sizex, sizez), midy+k*sizey, midz-k*Math.max(sizex, sizez));
      } else {
         this._camera.up.set(0, 0, 1);
         this._camera.position.set(midx-k*Math.max(sizex, sizey), midy-k*Math.max(sizex, sizey), midz+k*sizez);
      }

      if (this._camera.isOrthographicCamera && this.isOrthoCamera() && this._scene_width && this._scene_height) {
         const screen_ratio = this._scene_width / this._scene_height,
             szx = this._camera.right - this._camera.left, szy = this._camera.top - this._camera.bottom;

         if (screen_ratio > szx / szy) {
            // screen wider than actual geometry
            const m = (this._camera.right + this._camera.left) / 2;
            this._camera.left = m - szy * screen_ratio / 2;
            this._camera.right = m + szy * screen_ratio / 2;
         } else {
            // screen heigher than actual geometry
            const m = (this._camera.top + this._camera.bottom) / 2;
            this._camera.top = m + szx / screen_ratio / 2;
            this._camera.bottom = m - szx / screen_ratio / 2;
         }
      }

      this._camera.lookAt(this._lookat);
      this._camera.updateProjectionMatrix();

      this.changedLight(box);

      if (this._controls) {
         this._controls.target.copy(this._lookat);
         if (!only_set) this._controls.update();
      }

      // recheck which elements to draw
      if (this.ctrl.select_in_view && !only_set)
         this.startDrawGeometry();
   }

   /** @summary Specifies camera position as rotation around geometry center */
   setCameraPosition(rotatey, rotatez, zoom) {
      if (!this.ctrl) return;
      this.ctrl.rotatey = rotatey || 0;
      this.ctrl.rotatez = rotatez || 0;
      let preserve_zoom = false;
      if (zoom && Number.isFinite(zoom))
         this.ctrl.zoom = zoom;
       else
         preserve_zoom = true;

      this.adjustCameraPosition(false, preserve_zoom);
   }

   /** @summary Specifies camera position and point to which it looks to
       @desc Both specified in absolute coordinates */
   setCameraPositionAndLook(camx, camy, camz, lookx, looky, lookz) {
      if (!this.ctrl)
         return;
      this.ctrl.camx = camx;
      this.ctrl.camy = camy;
      this.ctrl.camz = camz;
      this.ctrl.camlx = lookx;
      this.ctrl.camly = looky;
      this.ctrl.camlz = lookz;
      this.adjustCameraPosition(false);
   }

   /** @summary focus on item */
   focusOnItem(itemname) {
      if (!itemname || !this._clones) return;

      const stack = this._clones.findStackByName(itemname);

      if (stack)
         this.focusCamera(this._clones.resolveStack(stack, true), false);
   }

   /** @summary focus camera on speicifed position */
   focusCamera(focus, autoClip) {
      if (this.ctrl.project || this.isOrthoCamera()) {
         this.adjustCameraPosition(true);
         return this.render3D();
      }

      let box = new Box3();
      if (focus === undefined)
         box = this.getGeomBoundingBox();
       else if (focus instanceof Mesh)
         box.setFromObject(focus);
       else {
         const center = new Vector3().setFromMatrixPosition(focus.matrix),
             node = focus.node,
             halfDelta = new Vector3(node.fDX, node.fDY, node.fDZ).multiplyScalar(0.5);
         box.min = center.clone().sub(halfDelta);
         box.max = center.clone().add(halfDelta);
      }

      const 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;

      let position, frames = 50, step = 0;
      if (this.ctrl._yup)
         position = new Vector3(midx-2*Math.max(sizex, sizez), midy+2*sizey, midz-2*Math.max(sizex, sizez));
      else
         position = new Vector3(midx-2*Math.max(sizex, sizey), midy-2*Math.max(sizex, sizey), midz+2*sizez);

      const target = new Vector3(midx, midy, midz),
            oldTarget = this._controls.target,
            // Amount to change camera position at each step
            posIncrement = position.sub(this._camera.position).divideScalar(frames),
            // Amount to change 'lookAt' so it will end pointed at target
            targetIncrement = target.sub(oldTarget).divideScalar(frames);

      autoClip = autoClip && this._webgl;

      // Automatic Clipping
      if (autoClip) {
         for (let axis = 0; axis < 3; ++axis) {
            const cc = this.ctrl.clip[axis];
            if (!cc.enabled) { cc.value = cc.min; cc.enabled = true; }
            cc.inc = ((cc.min + cc.max) / 2 - cc.value) / frames;
         }
         this.updateClipping();
      }

      this._animating = true;

      // Interpolate //

      const animate = () => {
         if (this._animating === undefined) return;

         if (this._animating)
            requestAnimationFrame(animate);
          else {
            if (!this._geom_viewer)
               this.startDrawGeometry();
         }
         const smoothFactor = -Math.cos((2.0*Math.PI*step)/frames) + 1.0;
         this._camera.position.add(posIncrement.clone().multiplyScalar(smoothFactor));
         oldTarget.add(targetIncrement.clone().multiplyScalar(smoothFactor));
         this._lookat = oldTarget;
         this._camera.lookAt(this._lookat);
         this._camera.updateProjectionMatrix();

         const tm1 = new Date().getTime();
         if (autoClip) {
            for (let axis = 0; axis < 3; ++axis)
               this.ctrl.clip[axis].value += this.ctrl.clip[axis].inc * smoothFactor;
            this.updateClipping();
         } else
            this.render3D(0);

         const tm2 = new Date().getTime();
         if ((step === 0) && (tm2-tm1 > 200)) frames = 20;
         step++;
         this._animating = step < frames;
      };

      animate();

   //   this._controls.update();
   }

   /** @summary actiavte auto rotate */
   autorotate(speed) {
      const rotSpeed = (speed === undefined) ? 2.0 : speed;
      let last = new Date();

      const animate = () => {
         if (!this._renderer || !this.ctrl) return;

         const current = new Date();

         if (this.ctrl.rotate)
            requestAnimationFrame(animate);

         if (this._controls) {
            this._controls.autoRotate = this.ctrl.rotate;
            this._controls.autoRotateSpeed = rotSpeed * (current.getTime() - last.getTime()) / 16.6666;
            this._controls.update();
         }
         last = new Date();
         this.render3D(0);
      };

      if (this._webgl) animate();
   }

   /** @summary called at the end of scene drawing */
   completeScene() {
   }

   /** @summary Drawing with 'count' option
     * @desc Scans hieararchy and check for unique nodes
     * @return {Promise} with object drawing ready */
   async drawCount(unqievis, clonetm) {
      const makeTime = tm => (this.isBatchMode() ? 'anytime' : tm.toString()) + ' ms',

       res = ['Unique nodes: ' + this._clones.nodes.length,
                  'Unique visible: ' + unqievis,
                  'Time to clone: ' + makeTime(clonetm)];

      // need to fill cached value line numvischld
      this._clones.scanVisible();

      let nshapes = 0;
      const arg = {
         clones: this._clones,
         cnt: [],
         func(node) {
            if (this.cnt[this.last] === undefined)
               this.cnt[this.last] = 1;
            else
               this.cnt[this.last]++;

            nshapes += countNumShapes(this.clones.getNodeShape(node.id));
            return true;
         }
      };

      let tm1 = new Date().getTime(),
          numvis = this._clones.scanVisible(arg),
          tm2 = new Date().getTime();

      res.push(`Total visible nodes: ${numvis}`, `Total shapes: ${nshapes}`);

      for (let lvl = 0; lvl < arg.cnt.length; ++lvl) {
         if (arg.cnt[lvl] !== undefined)
            res.push(`  lvl${lvl}: ${arg.cnt[lvl]}`);
      }

      res.push(`Time to scan: ${makeTime(tm2-tm1)}`, '', 'Check timing for matrix calculations ...');

      const elem = this.selectDom().style('overflow', 'auto');

      if (this.isBatchMode())
         elem.property('_json_object_', res);
      else
         res.forEach(str => elem.append('p').text(str));

      return postponePromise(() => {
         arg.domatrix = true;
         tm1 = new Date().getTime();
         numvis = this._clones.scanVisible(arg);
         tm2 = new Date().getTime();

         const last_str = `Time to scan with matrix: ${makeTime(tm2-tm1)}`;
         if (this.isBatchMode())
            res.push(last_str);
         else
            elem.append('p').text(last_str);
         return this;
      }, 100);
   }

   /** @summary Handle drop operation
     * @desc opt parameter can include function name like opt$func_name
     * Such function should be possible to find via {@link findFunction}
     * Function has to return Promise with objects to draw on geometry
     * By default function with name 'extract_geo_tracks' is checked
     * @return {Promise} handling of drop operation */
   async performDrop(obj, itemname, hitem, opt) {
      if (obj?.$kind === 'TTree') {
         // drop tree means function call which must extract tracks from provided tree

         let funcname = 'extract_geo_tracks';

         if (opt && opt.indexOf('$') > 0) {
            funcname = opt.slice(0, opt.indexOf('$'));
            opt = opt.slice(opt.indexOf('$')+1);
         }

         const func = findFunction(funcname);

         if (!func) return Promise.reject(Error(`Function ${funcname} not found`));

         return func(obj, opt).then(tracks => {
            if (!tracks) return this;

            // FIXME: probably tracks should be remembered?
            return this.drawExtras(tracks, '', false).then(() => {
               this.updateClipping(true);
               return this.render3D(100);
            });
         });
      }

      return this.drawExtras(obj, itemname).then(is_any => {
         if (!is_any) return this;

         if (hitem) hitem._painter = this; // set for the browser item back pointer

         return this.render3D(100);
      });
   }

   /** @summary function called when mouse is going over the item in the browser */
   mouseOverHierarchy(on, itemname, hitem) {
      if (!this.ctrl) return; // protection for cleaned-up painter

      const obj = hitem._obj;

      // let's highlight tracks and hits only for the time being
      if (!obj || (obj._typename !== clTEveTrack && obj._typename !== clTEvePointSet && obj._typename !== clTPolyMarker3D)) return;

      this.highlightMesh(null, 0x00ff00, on ? obj : null);
   }

   /** @summary clear extra drawn objects like tracks or hits */
   clearExtras() {
      this.getExtrasContainer('delete');
      delete this._extraObjects; // workaround, later will be normal function
      this.render3D();
   }

   /** @summary Register extra objects like tracks or hits
    * @desc Rendered after main geometry volumes are created
    * Check if object already exists to prevent duplication */
   addExtra(obj, itemname) {
      if (this._extraObjects === undefined)
         this._extraObjects = create(clTList);

      if (this._extraObjects.arr.indexOf(obj) >= 0)
         return false;

      this._extraObjects.Add(obj, itemname);

      delete obj.$hidden_via_menu; // remove previous hidden property

      return true;
   }

   /** @summary manipulate visisbility of extra objects, used for HierarchyPainter
     * @private */
   extraObjectVisible(hpainter, hitem, toggle) {
      if (!this._extraObjects) return;

      const itemname = hpainter.itemFullName(hitem);
      let indx = this._extraObjects.opt.indexOf(itemname);

      if ((indx < 0) && hitem._obj) {
         indx = this._extraObjects.arr.indexOf(hitem._obj);
         // workaround - if object found, replace its name
         if (indx >= 0) this._extraObjects.opt[indx] = itemname;
      }

      if (indx < 0) return;

      const obj = this._extraObjects.arr[indx];
      let res = !!obj.$hidden_via_menu;

      if (toggle) {
         obj.$hidden_via_menu = res;
         res = !res;

         let mesh = null;
         // either found painted object or just draw once again
         this._toplevel.traverse(node => { if (node.geo_object === obj) mesh = node; });

         if (mesh) {
            mesh.visible = res;
            this.render3D();
         } else if (res) {
            this.drawExtras(obj, '', false).then(() => {
               this.updateClipping(true);
               this.render3D();
            });
         }
      }

      return res;
   }

   /** @summary Draw extra object like tracks
     * @return {Promise} for ready */
   async drawExtras(obj, itemname, add_objects, not_wait_render) {
      // if object was hidden via menu, do not redraw it with next draw call
      if (!obj?._typename || (!add_objects && obj.$hidden_via_menu))
         return false;

      let do_render = false;
      if (add_objects === undefined) {
         add_objects = true;
         do_render = true;
      } else if (not_wait_render)
         do_render = true;


      let promise = false;

      if ((obj._typename === clTList) || (obj._typename === clTObjArray)) {
         if (!obj.arr) return false;
         const parr = [];
         for (let n = 0; n < obj.arr.length; ++n) {
            const sobj = obj.arr[n];
            let sname = obj.opt ? obj.opt[n] : '';
            if (!sname) sname = (itemname || '<prnt>') + `/[${n}]`;
            parr.push(this.drawExtras(sobj, sname, add_objects));
         }
         promise = Promise.all(parr).then(ress => ress.indexOf(true) >= 0);
      } else if (obj._typename === 'Mesh') {
         // adding mesh as is
         this.addToExtrasContainer(obj);
         promise = Promise.resolve(true);
      } else if (obj._typename === 'TGeoTrack') {
         if (!add_objects || this.addExtra(obj, itemname))
            promise = this.drawGeoTrack(obj, itemname);
      } else if (obj._typename === clTPolyLine3D) {
         if (!add_objects || this.addExtra(obj, itemname))
            promise = this.drawPolyLine(obj, itemname);
      } else if ((obj._typename === clTEveTrack) || (obj._typename === `${nsREX}REveTrack`)) {
         if (!add_objects || this.addExtra(obj, itemname))
            promise = this.drawEveTrack(obj, itemname);
      } else if ((obj._typename === clTEvePointSet) || (obj._typename === `${nsREX}REvePointSet`) || (obj._typename === clTPolyMarker3D)) {
         if (!add_objects || this.addExtra(obj, itemname))
            promise = this.drawHit(obj, itemname);
      } else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
         if (!add_objects || this.addExtra(obj, itemname))
            promise = this.drawExtraShape(obj, itemname);
      }

      return getPromise(promise).then(is_any => {
         if (!is_any || !do_render)
            return is_any;

         this.updateClipping(true);

         const pr = this.render3D(100, not_wait_render ? 'nopromise' : false);

         return not_wait_render ? this : pr;
      });
   }

   /** @summary returns container for extra objects */
   getExtrasContainer(action, name) {
      if (!this._toplevel) return null;

      if (!name) name = 'tracks';

      let extras = null;
      const lst = [];
      for (let n = 0; n < this._toplevel.children.length; ++n) {
         const chld = this._toplevel.children[n];
         if (!chld._extras) continue;
         if (action === 'collect') { lst.push(chld); continue; }
         if (chld._extras === name) { extras = chld; break; }
      }

      if (action === 'collect') {
         for (let k = 0; k < lst.length; ++k)
            this._toplevel.remove(lst[k]);
         return lst;
      }

      if (action === 'delete') {
         if (extras) this._toplevel.remove(extras);
         disposeThreejsObject(extras);
         return null;
      }

      if ((action !== 'get') && !extras) {
         extras = new Object3D();
         extras._extras = name;
         this._toplevel.add(extras);
      }

      return extras;
   }

   /** @summary add object to extras container.
     * @desc If fail, dispose object */
   addToExtrasContainer(obj, name) {
      const container = this.getExtrasContainer('', name);
      if (container)
         container.add(obj);
       else {
         console.warn('Fail to add object to extras');
         disposeThreejsObject(obj);
      }
   }

   /** @summary drawing TGeoTrack */
   drawGeoTrack(track, itemname) {
      if (!track?.fNpoints) return false;

      const linewidth = browser.isWin ? 1 : (track.fLineWidth || 1), // line width not supported on windows
            color = getColor(track.fLineColor) || '#ff00ff',
            npoints = Math.round(track.fNpoints/4), // each track point has [x,y,z,t] coordinate
            buf = new Float32Array((npoints-1)*6),
            projv = this.ctrl.projectPos,
            projx = (this.ctrl.project === 'x'),
            projy = (this.ctrl.project === 'y'),
            projz = (this.ctrl.project === 'z');

      for (let k = 0, pos = 0; k < npoints-1; ++k, pos+=6) {
         buf[pos] = projx ? projv : track.fPoints[k*4];
         buf[pos+1] = projy ? projv : track.fPoints[k*4+1];
         buf[pos+2] = projz ? projv : track.fPoints[k*4+2];
         buf[pos+3] = projx ? projv : track.fPoints[k*4+4];
         buf[pos+4] = projy ? projv : track.fPoints[k*4+5];
         buf[pos+5] = projz ? projv : track.fPoints[k*4+6];
      }

      const lineMaterial = new LineBasicMaterial({ color, linewidth }),
            line = createLineSegments(buf, lineMaterial);

      line.defaultOrder = line.renderOrder = 1000000; // to bring line to the front
      line.geo_name = itemname;
      line.geo_object = track;
      line.hightlightWidthScale = 2;

      if (itemname?.indexOf('<prnt>/Tracks') === 0)
         line.main_track = true;

      this.addToExtrasContainer(line);

      return true;
   }

   /** @summary drawing TPolyLine3D */
   drawPolyLine(line, itemname) {
      if (!line) return false;

      const linewidth = browser.isWin ? 1 : (line.fLineWidth || 1),
            color = getColor(line.fLineColor) || '#ff00ff',
            npoints = line.fN,
            fP = line.fP,
            buf = new Float32Array((npoints-1)*6),
            projv = this.ctrl.projectPos,
            projx = (this.ctrl.project === 'x'),
            projy = (this.ctrl.project === 'y'),
            projz = (this.ctrl.project === 'z');

      for (let k = 0, pos = 0; k < npoints-1; ++k, pos += 6) {
         buf[pos] = projx ? projv : fP[k*3];
         buf[pos+1] = projy ? projv : fP[k*3+1];
         buf[pos+2] = projz ? projv : fP[k*3+2];
         buf[pos+3] = projx ? projv : fP[k*3+3];
         buf[pos+4] = projy ? projv : fP[k*3+4];
         buf[pos+5] = projz ? projv : fP[k*3+5];
      }

      const lineMaterial = new LineBasicMaterial({ color, linewidth }),
          line3d = createLineSegments(buf, lineMaterial);

      line3d.defaultOrder = line3d.renderOrder = 1000000; // to bring line to the front
      line3d.geo_name = itemname;
      line3d.geo_object = line;
      line3d.hightlightWidthScale = 2;

      this.addToExtrasContainer(line3d);

      return true;
   }

   /** @summary Drawing TEveTrack */
   drawEveTrack(track, itemname) {
      if (!track || (track.fN <= 0)) return false;

      const linewidth = browser.isWin ? 1 : (track.fLineWidth || 1),
            color = getColor(track.fLineColor) || '#ff00ff',
            buf = new Float32Array((track.fN-1)*6),
            projv = this.ctrl.projectPos,
            projx = (this.ctrl.project === 'x'),
            projy = (this.ctrl.project === 'y'),
            projz = (this.ctrl.project === 'z');

      for (let k = 0, pos = 0; k < track.fN-1; ++k, pos+=6) {
         buf[pos] = projx ? projv : track.fP[k*3];
         buf[pos+1] = projy ? projv : track.fP[k*3+1];
         buf[pos+2] = projz ? projv : track.fP[k*3+2];
         buf[pos+3] = projx ? projv : track.fP[k*3+3];
         buf[pos+4] = projy ? projv : track.fP[k*3+4];
         buf[pos+5] = projz ? projv : track.fP[k*3+5];
      }

      const lineMaterial = new LineBasicMaterial({ color, linewidth }),
            line = createLineSegments(buf, lineMaterial);

      line.defaultOrder = line.renderOrder = 1000000; // to bring line to the front
      line.geo_name = itemname;
      line.geo_object = track;
      line.hightlightWidthScale = 2;

      this.addToExtrasContainer(line);

      return true;
   }

   /** @summary Drawing different hits types like TPolyMarker3D */
   async drawHit(hit, itemname) {
      if (!hit || !hit.fN || (hit.fN < 0))
         return false;

      // make hit size scaling factor of overall geometry size
      // otherwise it is not possible to correctly see hits at all
      const nhits = hit.fN,
            projv = this.ctrl.projectPos,
            projx = (this.ctrl.project === 'x'),
            projy = (this.ctrl.project === 'y'),
            projz = (this.ctrl.project === 'z'),
            hit_scale = Math.max(hit.fMarkerSize * this.getOverallSize() * (this._dummy ? 0.015 : 0.005), 0.2),
            pnts = new PointsCreator(nhits, this._webgl, hit_scale);

      for (let i = 0; i < nhits; i++) {
         pnts.addPoint(projx ? projv : hit.fP[i*3],
                       projy ? projv : hit.fP[i*3+1],
                       projz ? projv : hit.fP[i*3+2]);
      }

      return pnts.createPoints({ color: getColor(hit.fMarkerColor) || '#0000ff', style: hit.fMarkerStyle }).then(mesh => {
         mesh.defaultOrder = mesh.renderOrder = 1000000; // to bring points to the front
         mesh.highlightScale = 2;
         mesh.geo_name = itemname;
         mesh.geo_object = hit;
         this.addToExtrasContainer(mesh);
         return true; // indicate that rendering should be done
      });
   }

   /** @summary Draw extra shape on the geometry */
   drawExtraShape(obj, itemname) {
      const mesh = build(obj);
      if (!mesh) return false;

      mesh.geo_name = itemname;
      mesh.geo_object = obj;

      this.addToExtrasContainer(mesh);
      return true;
   }

   /** @summary Serach for specified node
     * @private */
   findNodeWithVolume(name, action, prnt, itemname, volumes) {
      let first_level = false, res = null;

      if (!prnt) {
         prnt = this.getGeometry();
         if (!prnt && (getNodeKind(prnt) !== 0)) return null;
         itemname = this.geo_manager ? prnt.fName : '';
         first_level = true;
         volumes = [];
      } else {
         if (itemname) itemname += '/';
         itemname += prnt.fName;
      }

      if (!prnt.fVolume || prnt.fVolume._searched) return null;

      if (name.test(prnt.fVolume.fName)) {
         res = action({ node: prnt, item: itemname });
         if (res) return res;
      }

      prnt.fVolume._searched = true;
      volumes.push(prnt.fVolume);

      if (prnt.fVolume.fNodes) {
         for (let n = 0, len = prnt.fVolume.fNodes.arr.length; n < len; ++n) {
            res = this.findNodeWithVolume(name, action, prnt.fVolume.fNodes.arr[n], itemname, volumes);
            if (res) break;
         }
      }

      if (first_level) {
         for (let n = 0, len = volumes.length; n < len; ++n)
            delete volumes[n]._searched;
      }

      return res;
   }

   /** @summary Process script option - load and execute some gGeoManager-related calls */
   async loadMacro(script_name) {
      const result = { obj: this.getGeometry(), prefix: '' };

      if (this.geo_manager)
         result.prefix = result.obj.fName;

      if (!script_name || (script_name.length < 3) || (getNodeKind(result.obj) !== 0))
         return result;

      const mgr = {
            GetVolume: name => {
               const regexp = new RegExp('^'+name+'$'),
                   currnode = this.findNodeWithVolume(regexp, arg => arg);

               if (!currnode) console.log(`Did not found ${name} volume`);

               // return proxy object with several methods, typically used in ROOT geom scripts
               return {
                   found: currnode,
                   fVolume: currnode?.node?.fVolume,
                   InvisibleAll(flag) {
                      setInvisibleAll(this.fVolume, flag);
                   },
                   Draw() {
                      if (!this.found || !this.fVolume) return;
                      result.obj = this.found.node;
                      result.prefix = this.found.item;
                      console.log(`Select volume for drawing ${this.fVolume.fName} ${result.prefix}`);
                   },
                   SetTransparency(lvl) {
                     if (this.fVolume?.fMedium?.fMaterial)
                        this.fVolume.fMedium.fMaterial.fFillStyle = 3000 + lvl;
                   },
                   SetLineColor(col) {
                      if (this.fVolume) this.fVolume.fLineColor = col;
                   }
                };
            },

            DefaultColors: () => {
               this.ctrl.dflt_colors = true;
            },

            SetMaxVisNodes: limit => {
               if (!this.ctrl.maxnodes)
                  this.ctrl.maxnodes = parseInt(limit) || 0;
            },

            SetVisLevel: limit => {
               if (!this.ctrl.vislevel)
                  this.ctrl.vislevel = parseInt(limit) || 0;
            }
          };

      showProgress(`Loading macro ${script_name}`);

      return httpRequest(script_name, 'text').then(script => {
         const lines = script.split('\n');
         let indx = 0;

         while (indx < lines.length) {
            let line = lines[indx++].trim();

            if (line.indexOf('//') === 0) continue;

            if (line.indexOf('gGeoManager') < 0) continue;
            line = line.replace('->GetVolume', '.GetVolume');
            line = line.replace('->InvisibleAll', '.InvisibleAll');
            line = line.replace('->SetMaxVisNodes', '.SetMaxVisNodes');
            line = line.replace('->DefaultColors', '.DefaultColors');
            line = line.replace('->Draw', '.Draw');
            line = line.replace('->SetTransparency', '.SetTransparency');
            line = line.replace('->SetLineColor', '.SetLineColor');
            line = line.replace('->SetVisLevel', '.SetVisLevel');
            if (line.indexOf('->') >= 0) continue;

            try {
               const func = new Function('gGeoManager', line);
               func(mgr);
            } catch (err) {
               console.error(`Problem by processing ${line}`);
            }
         }

         return result;
      }).catch(() => {
         console.error(`Fail to load ${script_name}`);
         return result;
      });
   }

   /** @summary Assign clones, created outside.
     * @desc Used by geometry painter, where clones are handled by the server */
   assignClones(clones) {
      this._clones_owner = true;
      this._clones = clones;
   }

    /** @summary Extract shapes from draw message of geometry painter
      * @desc For the moment used in batch production */
   extractRawShapes(draw_msg, recreate) {
      let nodes = null, old_gradpersegm = 0;

      // array for descriptors for each node
      // if array too large (>1M), use JS object while only ~1K nodes are expected to be used
      if (recreate) {
         // if (draw_msg.kind !== 'draw') return false;
         nodes = (draw_msg.numnodes > 1e6) ? { length: draw_msg.numnodes } : new Array(draw_msg.numnodes); // array for all nodes
      }

      draw_msg.nodes.forEach(node => {
         node = ClonedNodes.formatServerElement(node);
         if (nodes)
            nodes[node.id] = node;
         else
            this._clones.updateNode(node);
      });

      if (recreate) {
         this._clones_owner = true;
         this._clones = new ClonedNodes(null, nodes);
         this._clones.name_prefix = this._clones.getNodeName(0);
         this._clones.setConfig(this.ctrl);

         // normally only need when making selection, not used in geo viewer
         // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes);
         // this.geo_clones.setVisLevel(draw_msg.vislevel);
         // TODO: provide from server
         this._clones.maxdepth = 20;
      }

      let nsegm = 0;
      if (draw_msg.cfg)
         nsegm = draw_msg.cfg.nsegm;

      if (nsegm) {
         old_gradpersegm = geoCfg('GradPerSegm');
         geoCfg('GradPerSegm', 360 / Math.max(nsegm, 6));
      }

      for (let cnt = 0; cnt < draw_msg.visibles.length; ++cnt) {
         const item = draw_msg.visibles[cnt], rd = item.ri;

         // entry may be provided without shape - it is ok
         if (rd)
            item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm);
      }

      if (old_gradpersegm)
         geoCfg('GradPerSegm', old_gradpersegm);

      return true;
   }

   /** @summary Prepare drawings
     * @desc Return value used as promise for painter */
   async prepareObjectDraw(draw_obj, name_prefix) {
      // if did cleanup - ignore all kind of activity
      if (this.did_cleanup)
         return null;

      if (name_prefix === '__geom_viewer_append__') {
         this._new_append_nodes = draw_obj;
         this.ctrl.use_worker = 0;
         this._geom_viewer = true; // indicate that working with geom viewer
      } else if ((name_prefix === '__geom_viewer_selection__') && this._clones) {
         // these are selection done from geom viewer
         this._new_draw_nodes = draw_obj;
         this.ctrl.use_worker = 0;
         this._geom_viewer = true; // indicate that working with geom viewer
      } else if (this._main_painter) {
         this._clones_owner = false;
         this._clones = this._main_painter._clones;
         console.log(`Reuse clones ${this._clones.nodes.length} from main painter`);
      } else if (!draw_obj) {
         this._clones_owner = false;
         this._clones = null;
      } else {
         this._start_drawing_time = new Date().getTime();
         this._clones_owner = true;
         this._clones = new ClonedNodes(draw_obj);
         let lvl = this.ctrl.vislevel, maxnodes = this.ctrl.maxnodes;
         if (this.geo_manager) {
            if (!lvl && this.geo_manager.fVisLevel)
               lvl = this.geo_manager.fVisLevel;
            if (!maxnodes)
               maxnodes = this.geo_manager.fMaxVisNodes;
         }

         this._clones.setVisLevel(lvl);
         this._clones.setMaxVisNodes(maxnodes, this.ctrl.more);
         this._clones.setConfig(this.ctrl);

         this._clones.name_prefix = name_prefix;

         const hide_top_volume = !!this.geo_manager && !this.ctrl.showtop;
         let uniquevis = this.ctrl.no_screen ? 0 : this._clones.markVisibles(true, false, hide_top_volume);

         if (uniquevis <= 0)
            uniquevis = this._clones.markVisibles(false, false, hide_top_volume);
         else
            uniquevis = this._clones.markVisibles(true, true, hide_top_volume); // copy bits once and use normal visibility bits

         this._clones.produceIdShifts();

         const spent = new Date().getTime() - this._start_drawing_time;

         if (!this._scene)
            console.log(`Creating clones ${this._clones.nodes.length} takes ${spent} ms uniquevis ${uniquevis}`);

         if (this.ctrl._count)
            return this.drawCount(uniquevis, spent);
      }

      let promise = Promise.resolve(true);

      if (!this._scene) {
         this._first_drawing = true;

         const pp = this.getPadPainter();

         this._on_pad = !!pp;

         if (this._on_pad) {
            let size, render3d, fp;
            promise = ensureTCanvas(this, '3d').then(() => {
               if (pp.fillatt?.color)
                  this.ctrl.background = pp.fillatt.color;
               fp = this.getFramePainter();

               this.batch_mode = pp.isBatchMode();

               render3d = getRender3DKind(undefined, this.batch_mode);
               assign3DHandler(fp);
               fp.mode3d = true;

               size = fp.getSizeFor3d(undefined, render3d);

               this._fit_main_area = (size.can3d === -1);

               return this.createScene(size.width, size.height, render3d)
                          .then(dom => fp.add3dCanvas(size, dom, render3d === constants.Render3D.WebGL));
            });
         } else {
            const dom = this.selectDom('origin');

            this.batch_mode = isBatchMode() || (!dom.empty() && dom.property('_batch_mode'));
            this.batch_format = dom.property('_batch_format');

            const render3d = getRender3DKind(this.options.Render3D, this.batch_mode);

            // activate worker
            if ((this.ctrl.use_worker > 0) && !this.batch_mode)
               this.startWorker();

            assign3DHandler(this);

            const size = this.getSizeFor3d(undefined, render3d);

            this._fit_main_area = (size.can3d === -1);

            promise = this.createScene(size.width, size.height, render3d)
                          .then(dom => this.add3dCanvas(size, dom, this._webgl));
         }
      }

      return promise.then(() => {
         // this is limit for the visible faces, number of volumes does not matter
         if (this._first_drawing && !this.ctrl.maxfaces)
            this.ctrl.maxfaces = 200000 * this.ctrl.more;

         // set top painter only when first child exists
         this.setAsMainPainter();

         this.createToolbar();

         // just draw extras and complete drawing if there are no main model
         if (!this._clones)
            return this.completeDraw();

         return new Promise(resolveFunc => {
            this._resolveFunc = resolveFunc;
            this.showDrawInfo('Drawing geometry');
            this.startDrawGeometry(true);
         });
      });
   }

   /** @summary methods show info when first geometry drawing is performed */
   showDrawInfo(msg) {
      if (this.isBatchMode() || !this._first_drawing || !this._start_drawing_time) return;

      const main = this._renderer.domElement.parentNode;
      if (!main) return;

      let info = main.querySelector('.geo_info');

      if (!msg)
         info?.remove();
       else {
         const spent = (new Date().getTime() - this._start_drawing_time)*1e-3;
         if (!info) {
            info = getDocument().createElement('p');
            info.setAttribute('class', 'geo_info');
            info.setAttribute('style', 'position: absolute; text-align: center; vertical-align: middle; top: 45%; left: 40%; color: red; font-size: 150%;');
            main.append(info);
         }
         info.innerHTML = `${msg}, ${spent.toFixed(1)}s`;
      }
   }

   /** @summary Reentrant method to perform geometry drawing step by step */
   continueDraw() {
      // nothing to do - exit
      if (this.isStage(stageInit)) return;

      const tm0 = new Date().getTime(),
            interval = this._first_drawing ? 1000 : 200;
      let now = tm0;

      while (true) {
         const res = this.nextDrawAction();
         if (!res) break;

         now = new Date().getTime();

         // stop creation after 100 sec, render as is
         if (now - this._startm > 1e5) {
            this.changeStage(stageInit, 'Abort build after 100s');
            break;
         }

         // if we are that fast, do next action
         if ((res === true) && (now - tm0 < interval)) continue;

         if ((now - tm0 > interval) || (res === 1) || (res === 2)) {
            showProgress(this.drawing_log);

            this.showDrawInfo(this.drawing_log);

            if (this._first_drawing && this._webgl && (this._num_meshes - this._last_render_meshes > 100) && (now - this._last_render_tm > 2.5*interval)) {
               this.adjustCameraPosition();
               this.render3D(-1);
               this._last_render_meshes = this.ctrl.info.num_meshes;
            }
            if (res !== 2) setTimeout(() => this.continueDraw(), (res === 1) ? 100 : 1);

            return;
         }
      }

      const take_time = now - this._startm;

      if (this._first_drawing || this._full_redrawing)
         console.log(`Create tm = ${take_time} meshes ${this.ctrl.info.num_meshes} faces ${this.ctrl.info.num_faces}`);

      if (take_time > 300) {
         showProgress('Rendering geometry');
         this.showDrawInfo('Rendering');
         return setTimeout(() => this.completeDraw(true), 10);
      }

      this.completeDraw(true);
   }

   /** @summary Checks camera position and recalculate rendering order if needed
     * @param force - if specified, forces calculations of render order */
   testCameraPosition(force) {
      this._camera.updateMatrixWorld();

      this.drawOverlay();

      const origin = this._camera.position.clone();
      if (!force && this._last_camera_position) {
         // if camera position does not changed a lot, ignore such change
         const dist = this._last_camera_position.distanceTo(origin);
         if (dist < (this._overall_size || 1000)*1e-4) return;
      }

      this._last_camera_position = origin; // remember current camera position

      if (this.ctrl._axis) {
         const vect = (this._controls?.target || this._lookat).clone().sub(this._camera.position).normalize();
         this.getExtrasContainer('get', 'axis')?.traverse(obj3d => {
            if (isFunc(obj3d._axis_flip))
               obj3d._axis_flip(vect);
         });
      }

      if (!this.ctrl.project)
         produceRenderOrder(this._toplevel, origin, this.ctrl.depthMethod, this._clones);
   }

   /** @summary Call 3D rendering of the geometry
     * @param tmout - specifies delay, after which actual rendering will be invoked
     * @param [measure] - when true, for the first time printout rendering time
     * @return {Promise} when tmout bigger than 0 is specified
     * @desc Timeout used to avoid multiple rendering of the picture when several 3D drawings
     * superimposed with each other. If tmeout <= 0, rendering performed immediately
     * Several special values are used:
     *   -1    - force recheck of rendering order based on camera position */
   render3D(tmout, measure) {
      if (!this._renderer) {
         if (!this.did_cleanup)
            console.warn('renderer object not exists - check code');
         else
            console.warn('try to render after cleanup');
         return this;
      }

      const ret_promise = (tmout !== undefined) && (tmout > 0) && (measure !== 'nopromise');

      if (tmout === undefined) tmout = 5; // by default, rendering happens with timeout

      if ((tmout > 0) && this._webgl) {
         if (this.isBatchMode()) tmout = 1; // use minimal timeout in batch mode
         if (ret_promise) {
            return new Promise(resolveFunc => {
               if (!this._render_resolveFuncs)
                  this._render_resolveFuncs = [];
               this._render_resolveFuncs.push(resolveFunc);
               if (!this.render_tmout)
                  this.render_tmout = setTimeout(() => this.render3D(0), tmout);
            });
         }

         if (!this.render_tmout)
            this.render_tmout = setTimeout(() => this.render3D(0), tmout);
         return this;
      }

      if (this.render_tmout) {
         clearTimeout(this.render_tmout);
         delete this.render_tmout;
      }

      beforeRender3D(this._renderer);

      const tm1 = new Date();

      if (this._adjust_camera_with_render) {
         this.adjustCameraPosition('only_set');
         delete this._adjust_camera_with_render;
      }

      this.testCameraPosition(tmout === -1);

      // its needed for outlinePass - do rendering, most consuming time
      if (this._webgl && this._effectComposer && (this._effectComposer.passes.length > 0))
         this._effectComposer.render();
       else if (this._webgl && this._bloomComposer && (this._bloomComposer.passes.length > 0)) {
         this._renderer.clear();
         this._camera.layers.set(_BLOOM_SCENE);
         this._bloomComposer.render();
         this._renderer.clearDepth();
         this._camera.layers.set(_ENTIRE_SCENE);
         this._renderer.render(this._scene, this._camera);
      } else
         this._renderer.render(this._scene, this._camera);


      const tm2 = new Date();

      this.last_render_tm = tm2.getTime();

      if ((this.first_render_tm === 0) && (measure === true)) {
         this.first_render_tm = tm2.getTime() - tm1.getTime();
         if (this.first_render_tm > 500)
            console.log(`three.js r${REVISION}, first render tm = ${this.first_render_tm}`);
      }

      afterRender3D(this._renderer);

      if (this._render_resolveFuncs) {
         const arr = this._render_resolveFuncs;
         delete this._render_resolveFuncs;
         arr.forEach(func => func(this));
      }
   }

   /** @summary Start geo worker */
   startWorker() {
      if (this._worker) return;

      this._worker_ready = false;
      this._worker_jobs = 0; // counter how many requests send to worker

      // TODO: modules not yet working, see https://www.codedread.com/blog/archives/2017/10/19/web-workers-can-be-es6-modules-too/
      this._worker = new Worker(source_dir + 'scripts/geoworker.js' /*, { type: 'module' } */);

      this._worker.onmessage = e => {
         if (!isObject(e.data)) return;

         if ('log' in e.data)
            return console.log(`geo: ${e.data.log}`);

         if ('progress' in e.data)
            return showProgress(e.data.progress);

         e.data.tm3 = new Date().getTime();

         if ('init' in e.data) {
            this._worker_ready = true;
            console.log(`Worker ready: ${e.data.tm3 - e.data.tm0}`);
         } else
            this.processWorkerReply(e.data);
      };

      // send initialization message with clones
      this._worker.postMessage({
         init: true,   // indicate init command for worker
         browser,
         tm0: new Date().getTime(),
         vislevel: this._clones.getVisLevel(),
         maxvisnodes: this._clones.getMaxVisNodes(),
         clones: this._clones.nodes,
         sortmap: this._clones.sortmap
      });
   }

   /** @summary check if one can submit request to worker
     * @private */
   canSubmitToWorker(force) {
      if (!this._worker) return false;

      return this._worker_ready && ((this._worker_jobs === 0) || force);
   }

   /** @summary submit request to worker
     * @private */
   submitToWorker(job) {
      if (!this._worker) return false;

      this._worker_jobs++;
      job.tm0 = new Date().getTime();
      this._worker.postMessage(job);
   }

   /** @summary process reply from worker
     * @private */
   processWorkerReply(job) {
      this._worker_jobs--;

      if ('collect' in job) {
         this._new_draw_nodes = job.new_nodes;
         this._draw_all_nodes = job.complete;
         this.changeStage(stageAnalyze);
         // invoke methods immediately
         return this.continueDraw();
      }

      if ('shapes' in job) {
         for (let n=0; n<job.shapes.length; ++n) {
            const item = job.shapes[n],
                origin = this._build_shapes[n];

            // let shape = this._clones.getNodeShape(item.nodeid);

            if (item.buf_pos && item.buf_norm) {
               if (item.buf_pos.length === 0)
                  origin.geom = null;
                else if (item.buf_pos.length !== item.buf_norm.length) {
                  console.error(`item.buf_pos.length ${item.buf_pos.length} !== item.buf_norm.length ${item.buf_norm.length}`);
                  origin.geom = null;
               } else {
                  origin.geom = new BufferGeometry();

                  origin.geom.setAttribute('position', new BufferAttribute(item.buf_pos, 3));
                  origin.geom.setAttribute('normal', new BufferAttribute(item.buf_norm, 3));
               }

               origin.ready = true;
               origin.nfaces = item.nfaces;
            }
         }

         job.tm4 = new Date().getTime();

         this.changeStage(stageBuild); // first check which shapes are used, than build meshes

         // invoke methods immediately
         return this.continueDraw();
      }
   }

   /** @summary start draw geometries on master and all slaves
     * @private */
   testGeomChanges() {
      if (this._main_painter) {
         console.warn('Get testGeomChanges call for slave painter');
         return this._main_painter.testGeomChanges();
      }
      this.startDrawGeometry();
      for (let k = 0; k < this._slave_painters.length; ++k)
         this._slave_painters[k].startDrawGeometry();
   }

   /** @summary Draw axes and camera overlay */
   drawAxesAndOverlay(norender) {
      const res1 = this.drawAxes(),
          res2 = this.drawOverlay();

      if (!res1 && !res2)
         return norender ? null : this.render3D();
      else
         return this.changedDepthMethod(norender ? 'norender' : undefined);
   }

   /** @summary Draw overlay for the orthographic cameras */
   drawOverlay() {
      this.getExtrasContainer('delete', 'overlay');
      if (!this.isOrthoCamera() || (this.ctrl.camera_overlay === 'none'))
         return false;

      const zoom = 0.5 / this._camera.zoom,
          midx = (this._camera.left + this._camera.right) / 2,
          midy = (this._camera.bottom + this._camera.top) / 2,
          xmin = midx - (this._camera.right - this._camera.left) * zoom,
          xmax = midx + (this._camera.right - this._camera.left) * zoom,
          ymin = midy - (this._camera.top - this._camera.bottom) * zoom,
          ymax = midy + (this._camera.top - this._camera.bottom) * zoom,
          tick_size = (ymax - ymin) * 0.02,
          text_size = (ymax - ymin) * 0.015,
          grid_gap = (ymax - ymin) * 0.001,
          x1 = xmin + text_size * 5, x2 = xmax - text_size * 5,
          y1 = ymin + text_size * 3, y2 = ymax - text_size * 3,
          x_handle = new TAxisPainter(null, create(clTAxis));

      x_handle.configureAxis('xaxis', x1, x2, x1, x2, false, [x1, x2],
                             { log: 0, reverse: false });
      const y_handle = new TAxisPainter(null, create(clTAxis));
      y_handle.configureAxis('yaxis', y1, y2, y1, y2, false, [y1, y2],
                              { log: 0, reverse: false });

      const ii = this._camera.orthoIndicies ?? [0, 1, 2];
      let buf, pos, midZ = 0, gridZ = 0;

      if (this._camera.orthoZ)
         gridZ = midZ = this._camera.orthoZ[0];

      const addPoint = (x, y, z) => {
         buf[pos+ii[0]] = x;
         buf[pos+ii[1]] = y;
         buf[pos+ii[2]] = z ?? gridZ;
         pos += 3;
      }, createText = (lbl, size) => {
         const text3d = new TextGeometry(lbl, { font: HelveticerRegularFont, size, height: 0, curveSegments: 5 });
         text3d.computeBoundingBox();
         text3d._width = text3d.boundingBox.max.x - text3d.boundingBox.min.x;
         text3d._height = text3d.boundingBox.max.y - text3d.boundingBox.min.y;

         text3d.translate(-text3d._width/2, -text3d._height/2, 0);
         if (this._camera.orthoSign < 0)
            text3d.rotateY(Math.PI);

         if (isFunc(this._camera.orthoRotation))
            this._camera.orthoRotation(text3d);

         return text3d;
      }, createTextMesh = (geom, material, x, y, z) => {
         const tgt = [0, 0, 0];
         tgt[ii[0]] = x;
         tgt[ii[1]] = y;
         tgt[ii[2]] = z ?? gridZ;
         const mesh = new Mesh(geom, material);
         mesh.translateX(tgt[0]).translateY(tgt[1]).translateZ(tgt[2]);
         return mesh;
      };

      if (this.ctrl.camera_overlay === 'bar') {
         const container = this.getExtrasContainer('create', 'overlay');

         let x1 = xmin * 0.15 + xmax * 0.85,
             x2 = xmin * 0.05 + xmax * 0.95;
         const y1 = ymax * 0.9 + ymin * 0.1,
               y2 = ymax * 0.86 + ymin * 0.14,
               ticks = x_handle.createTicks();

         if (ticks.major?.length > 1) {
            x1 = ticks.major[ticks.major.length-2];
            x2 = ticks.major[ticks.major.length-1];
         }

         buf = new Float32Array(3*6); pos = 0;

         addPoint(x1, y1, midZ);
         addPoint(x1, y2, midZ);

         addPoint(x1, (y1 + y2) / 2, midZ);
         addPoint(x2, (y1 + y2) / 2, midZ);

         addPoint(x2, y1, midZ);
         addPoint(x2, y2, midZ);

         const lineMaterial = new LineBasicMaterial({ color: 'green' }),
               textMaterial = new MeshBasicMaterial({ color: 'green', vertexColors: false });

         container.add(createLineSegments(buf, lineMaterial));

         const text3d = createText(x_handle.format(x2-x1, true), Math.abs(y2-y1));

         container.add(createTextMesh(text3d, textMaterial, (x2 + x1) / 2, (y1 + y2) / 2 + text3d._height * 0.8, midZ));
         return true;
      }

      const show_grid = this.ctrl.camera_overlay.indexOf('grid') === 0;

      if (show_grid && this._camera.orthoZ) {
         if (this.ctrl.camera_overlay === 'gridf')
            gridZ += this._camera.orthoSign * this._camera.orthoZ[1];
         else if (this.ctrl.camera_overlay === 'gridb')
            gridZ -= this._camera.orthoSign * this._camera.orthoZ[1];
      }

      if ((this.ctrl.camera_overlay === 'axis') || show_grid) {
         const container = this.getExtrasContainer('create', 'overlay'),
               lineMaterial = new LineBasicMaterial({ color: new Color('black') }),
               gridMaterial1 = show_grid ? new LineBasicMaterial({ color: new Color(0xbbbbbb) }) : null,
               gridMaterial2 = show_grid ? new LineDashedMaterial({ color: new Color(0xdddddd), dashSize: grid_gap, gapSize: grid_gap }) : null,
               textMaterial = new MeshBasicMaterial({ color: 'black', vertexColors: false }),
               xticks = x_handle.createTicks();

         while (xticks.next()) {
            const x = xticks.tick, k = (xticks.kind === 1) ? 1.0 : 0.6;

            if (show_grid) {
               buf = new Float32Array(2*3); pos = 0;
               addPoint(x, ymax - k*tick_size - grid_gap);
               addPoint(x, ymin + k*tick_size + grid_gap);
               container.add(createLineSegments(buf, xticks.kind === 1 ? gridMaterial1 : gridMaterial2));
            }

            buf = new Float32Array(4*3); pos = 0;
            addPoint(x, ymax);
            addPoint(x, ymax - k*tick_size);
            addPoint(x, ymin);
            addPoint(x, ymin + k*tick_size);

            container.add(createLineSegments(buf, lineMaterial));

            if (xticks.kind !== 1) continue;

            const text3d = createText(x_handle.format(x, true), text_size);

            container.add(createTextMesh(text3d, textMaterial, x, ymax - tick_size - text_size/2 - text3d._height/2));

            container.add(createTextMesh(text3d, textMaterial, x, ymin + tick_size + text_size/2 + text3d._height/2));
         }

         const yticks = y_handle.createTicks();

         while (yticks.next()) {
            const y = yticks.tick, k = (yticks.kind === 1) ? 1.0 : 0.6;

            if (show_grid) {
               buf = new Float32Array(2*3); pos = 0;
               addPoint(xmin + k*tick_size + grid_gap, y);
               addPoint(xmax - k*tick_size - grid_gap, y);
               container.add(createLineSegments(buf, yticks.kind === 1 ? gridMaterial1 : gridMaterial2));
            }

            buf = new Float32Array(4*3); pos = 0;
            addPoint(xmin, y);
            addPoint(xmin + k*tick_size, y);
            addPoint(xmax, y);
            addPoint(xmax - k*tick_size, y);

            container.add(createLineSegments(buf, lineMaterial));

            if (yticks.kind !== 1) continue;

            const text3d = createText(y_handle.format(y, true), text_size);

            container.add(createTextMesh(text3d, textMaterial, xmin + tick_size + text_size/2 + text3d._width/2, y));

            container.add(createTextMesh(text3d, textMaterial, xmax - tick_size - text_size/2 - text3d._width/2, y));
         }

         return true;
      }

      return false;
   }

   /** @summary Draw axes if configured, otherwise just remove completely */
   drawAxes() {
      this.getExtrasContainer('delete', 'axis');

      if (!this.ctrl._axis)
         return false;

      const box = this.getGeomBoundingBox(this._toplevel, this.superimpose ? 'original' : undefined),
          container = this.getExtrasContainer('create', 'axis'),
          text_size = 0.02 * Math.max(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z),
          center = [0, 0, 0],
          names = ['x', 'y', 'z'],
          labels = ['X', 'Y', 'Z'],
          colors = ['red', 'green', 'blue'],
          ortho = this.isOrthoCamera(),
          ckind = this.ctrl.camera_kind ?? 'perspective';

      if (this.ctrl._axis === 2) {
         for (let naxis = 0; naxis < 3; ++naxis) {
            const name = names[naxis];
            if ((box.min[name] <= 0) && (box.max[name] >= 0)) continue;
            center[naxis] = (box.min[name] + box.max[name])/2;
         }
      }

      for (let naxis = 0; naxis < 3; ++naxis) {
         // exclude axis which is not seen
         if (ortho && ckind.indexOf(labels[naxis]) < 0) continue;

         const buf = new Float32Array(6),
             color = colors[naxis],
             name = names[naxis],

          valueToString = val => {
            if (!val) return '0';
            const lg = Math.log10(Math.abs(val));
            if (lg < 0) {
               if (lg > -1) return val.toFixed(2);
               if (lg > -2) return val.toFixed(3);
            } else {
               if (lg < 2) return val.toFixed(1);
               if (lg < 4) return val.toFixed(0);
            }
            return val.toExponential(2);
         },

          lbl = valueToString(box.max[name]) + ' ' + labels[naxis];

         buf[0] = box.min.x;
         buf[1] = box.min.y;
         buf[2] = box.min.z;

         buf[3] = box.min.x;
         buf[4] = box.min.y;
         buf[5] = box.min.z;

         switch (naxis) {
           case 0: buf[3] = box.max.x; break;
           case 1: buf[4] = box.max.y; break;
           case 2: buf[5] = box.max.z; break;
         }

         if (this.ctrl._axis === 2) {
            for (let k = 0; k < 6; ++k)
               if ((k % 3) !== naxis) buf[k] = center[k%3];
         }

         const lineMaterial = new LineBasicMaterial({ color });
         let mesh = createLineSegments(buf, lineMaterial);

         mesh._no_clip = true; // skip from clipping

         container.add(mesh);

         const textMaterial = new MeshBasicMaterial({ color, vertexColors: false });

         if ((center[naxis] === 0) && (center[naxis] >= box.min[name]) && (center[naxis] <= box.max[name])) {
            if ((this.ctrl._axis !== 2) || (naxis === 0)) {
               const geom = ortho ? new CircleGeometry(text_size*0.25) : new SphereGeometry(text_size*0.25);
               mesh = new Mesh(geom, textMaterial);
               mesh.translateX(naxis === 0 ? center[0] : buf[0]);
               mesh.translateY(naxis === 1 ? center[1] : buf[1]);
               mesh.translateZ(naxis === 2 ? center[2] : buf[2]);
               mesh._no_clip = true;
               container.add(mesh);
            }
         }

         let text3d = new TextGeometry(lbl, { font: HelveticerRegularFont, size: text_size, height: 0, curveSegments: 5 });
         mesh = new Mesh(text3d, textMaterial);
         mesh._no_clip = true; // skip from clipping

         function setSideRotation(mesh, normal) {
            mesh._other_side = false;
            mesh._axis_norm = normal ?? new Vector3(1, 0, 0);
            mesh._axis_flip = function(vect) {
               const other_side = vect.dot(this._axis_norm) < 0;
               if (this._other_side !== other_side) {
                  this._other_side = other_side;
                  this.rotateY(Math.PI);
               }
            };
         }

         function setTopRotation(mesh, first_angle = -1) {
            mesh._last_angle = first_angle;
            mesh._axis_flip = function(vect) {
               let angle = 0;
               switch (this._axis_name) {
                  case 'x': angle = -Math.atan2(vect.y, vect.z); break;
                  case 'y': angle = -Math.atan2(vect.z, vect.x); break;
                  default: angle = Math.atan2(vect.y, vect.x);
               }
               angle = Math.round(angle / Math.PI * 2 + 2) % 4;
               if (this._last_angle !== angle) {
                  this.rotateX((angle - this._last_angle) * Math.PI/2);
                  this._last_angle = angle;
               }
            };
         }

         let textbox = new Box3().setFromObject(mesh);

         text3d.translate(-textbox.max.x*0.5, -textbox.max.y/2, 0);

         mesh.translateX(buf[3]);
         mesh.translateY(buf[4]);
         mesh.translateZ(buf[5]);

         mesh._axis_name = name;

         if (naxis === 0) {
            if (ortho && ckind.indexOf('OX') > 0)
               setTopRotation(mesh, 0);
             else if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup)
               setSideRotation(mesh, new Vector3(0, 0, -1));
             else {
               setSideRotation(mesh, new Vector3(0, 1, 0));
               mesh.rotateX(Math.PI/2);
            }

            mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
         } else if (naxis === 1) {
            if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup) {
               setTopRotation(mesh, 2);
               mesh.rotateX(-Math.PI/2);
               mesh.rotateY(-Math.PI/2);
               mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
            } else {
               setSideRotation(mesh);
               mesh.rotateX(Math.PI/2);
               mesh.rotateY(-Math.PI/2);
               mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
            }
         } else if (naxis === 2) {
            if (ortho ? ckind.indexOf('OZ') < 0 : this.ctrl._yup) {
               const zox = ortho && (ckind.indexOf('ZOX') > 0 || ckind.indexOf('ZNOX') > 0);
               setSideRotation(mesh, zox ? new Vector3(0, -1, 0) : undefined);
               mesh.rotateY(-Math.PI/2);
               if (zox) mesh.rotateX(-Math.PI/2);
            } else {
               setTopRotation(mesh);
               mesh.rotateX(Math.PI/2);
               mesh.rotateZ(Math.PI/2);
            }
            mesh.translateX(text_size*0.5 + textbox.max.x*0.5);
         }

         container.add(mesh);

         text3d = new TextGeometry(valueToString(box.min[name]), { font: HelveticerRegularFont, size: text_size, height: 0, curveSegments: 5 });

         mesh = new Mesh(text3d, textMaterial);
         mesh._no_clip = true; // skip from clipping
         textbox = new Box3().setFromObject(mesh);

         text3d.translate(-textbox.max.x*0.5, -textbox.max.y/2, 0);

         mesh._axis_name = name;

         mesh.translateX(buf[0]);
         mesh.translateY(buf[1]);
         mesh.translateZ(buf[2]);

         if (naxis === 0) {
            if (ortho && ckind.indexOf('OX') > 0)
               setTopRotation(mesh, 0);
             else if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup)
               setSideRotation(mesh, new Vector3(0, 0, -1));
             else {
               setSideRotation(mesh, new Vector3(0, 1, 0));
               mesh.rotateX(Math.PI/2);
            }
            mesh.translateX(-text_size*0.5 - textbox.max.x*0.5);
         } else if (naxis === 1) {
            if (ortho ? ckind.indexOf('OY') > 0 : this.ctrl._yup) {
               setTopRotation(mesh, 2);
               mesh.rotateX(-Math.PI/2);
               mesh.rotateY(-Math.PI/2);
               mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
            } else {
               setSideRotation(mesh);
               mesh.rotateX(Math.PI/2);
               mesh.rotateY(-Math.PI/2);
               mesh.translateX(textbox.max.x*0.5 + text_size*0.5);
            }
         } else if (naxis === 2) {
            if (ortho ? ckind.indexOf('OZ') < 0 : this.ctrl._yup) {
               const zox = ortho && (ckind.indexOf('ZOX') > 0 || ckind.indexOf('ZNOX') > 0);
               setSideRotation(mesh, zox ? new Vector3(0, -1, 0) : undefined);
               mesh.rotateY(-Math.PI/2);
               if (zox) mesh.rotateX(-Math.PI/2);
            } else {
               setTopRotation(mesh);
               mesh.rotateX(Math.PI/2);
               mesh.rotateZ(Math.PI/2);
            }
            mesh.translateX(-textbox.max.x*0.5 - text_size*0.5);
         }

         container.add(mesh);
      }

      // after creating axes trigger rendering and recalculation of depth
      return true;
   }

   /** @summary Set axes visibility 0 - off, 1 - on, 2 - centered */
   setAxesDraw(on) {
      if (on === 'toggle')
         this.ctrl._axis = this.ctrl._axis ? 0 : 1;
      else
         this.ctrl._axis = (typeof on === 'number') ? on : (on ? 1 : 0);
      return this.drawAxesAndOverlay();
   }

   /** @summary Set auto rotate mode */
   setAutoRotate(on) {
      if (this.ctrl.project) return;
      if (on !== undefined) this.ctrl.rotate = on;
      this.autorotate(2.5);
   }

   /** @summary Toggle wireframe mode */
   toggleWireFrame() {
      this.ctrl.wireframe = !this.ctrl.wireframe;
      this.changedWireFrame();
   }

   /** @summary Specify wireframe mode */
   setWireFrame(on) {
      this.ctrl.wireframe = !!on;
      this.changedWireFrame();
   }

   /** @summary Specify showtop draw options, relevant only for TGeoManager */
   setShowTop(on) {
      this.ctrl.showtop = !!on;
      this.redrawObject('same');
   }

   /** @summary Should be called when configuration of particular axis is changed */
   changedClipping(naxis = -1) {
      if ((naxis < 0) || this.ctrl.clip[naxis]?.enabled)
         this.updateClipping(false, true);
   }

   /** @summary Should be called when depth test flag is changed */
   changedDepthTest() {
      if (!this._toplevel) return;
      const flag = this.ctrl.depthTest;
      this._toplevel.traverse(node => {
         if (node instanceof Mesh)
            node.material.depthTest = flag;
      });

      this.render3D(0);
   }

   /** @summary Should be called when depth method is changed */
   changedDepthMethod(arg) {
      // force recalculatiion of render order
      delete this._last_camera_position;
      if (arg !== 'norender')
         return this.render3D();
   }

   /** @summary Assign clipping attributes to the meshes - supported only for webgl */
   updateClipping(without_render, force_traverse) {
      // do not try clipping with SVG renderer
      if (this._renderer?.jsroot_render3d === constants.Render3D.SVG) return;

      if (!this._clipPlanes) {
         this._clipPlanes = [new Plane(new Vector3(1, 0, 0), 0),
                             new Plane(new Vector3(0, this.ctrl._yup ? -1 : 1, 0), 0),
                             new Plane(new Vector3(0, 0, this.ctrl._yup ? 1 : -1), 0)];
      }

      const clip = this.ctrl.clip,
            clip_constants = [-1 * clip[0].value, clip[1].value, (this.ctrl._yup ? -1 : 1) * clip[2].value],
            container = this.getExtrasContainer(this.ctrl.clipVisualize ? '' : 'delete', 'clipping');
      let panels = [], changed = false,
          clip_cfg = this.ctrl.clipIntersect ? 16 : 0;

      for (let k = 0; k < 3; ++k) {
         if (clip[k].enabled)
            clip_cfg += 2 << k;
         if (this._clipPlanes[k].constant !== clip_constants[k]) {
            if (clip[k].enabled) changed = true;
            this._clipPlanes[k].constant = clip_constants[k];
         }
         if (clip[k].enabled)
            panels.push(this._clipPlanes[k]);

         if (container && clip[k].enabled) {
            const helper = new PlaneHelper(this._clipPlanes[k], (clip[k].max - clip[k].min));
            helper._no_clip = true;
            container.add(helper);
         }
      }
      if (panels.length === 0)
         panels = null;

      if (this._clipCfg !== clip_cfg)
         changed = true;

      this._clipCfg = clip_cfg;

      const any_clipping = !!panels, ci = this.ctrl.clipIntersect,
          material_side = any_clipping ? DoubleSide : FrontSide;

      if (force_traverse || changed) {
         this._scene.traverse(node => {
            if (!node._no_clip && (node.material?.clippingPlanes !== undefined)) {
               if (node.material.clippingPlanes !== panels) {
                  node.material.clipIntersection = ci;
                  node.material.clippingPlanes = panels;
                  node.material.needsUpdate = true;
               }

               if (node.material.emissive !== undefined) {
                  if (node.material.side !== material_side) {
                     node.material.side = material_side;
                     node.material.needsUpdate = true;
                  }
               }
            }
         });
      }

      this.ctrl.doubleside = any_clipping;

      if (!without_render) this.render3D(0);

      return changed;
   }

   /** @summary Assign callback, invoked every time when drawing is completed
     * @desc Used together with web-based geometry viewer
     * @private */
   setCompleteHandler(callback) {
      this._complete_handler = callback;
   }

   /** @summary Completes drawing procedure
     * @return {Promise} for ready */
   async completeDraw(close_progress) {
      let first_time = false, full_redraw = false, check_extras = true;

      if (!this.ctrl) {
         console.warn('ctrl object does not exist in completeDraw - something went wrong');
         return this;
      }

      let promise = Promise.resolve(true);

      if (!this._clones) {
         check_extras = false;
         // if extra object where append, redraw them at the end
         this.getExtrasContainer('delete'); // delete old container
         const extras = (this._main_painter ? this._main_painter._extraObjects : null) || this._extraObjects;
         promise = this.drawExtras(extras, '', false);
      } else if (this._first_drawing || this._full_redrawing) {
         if (this.ctrl.tracks && this.geo_manager)
            promise = this.drawExtras(this.geo_manager.fTracks, '<prnt>/Tracks');
      }

      return promise.then(() => {
         if (this._full_redrawing) {
            this.adjustCameraPosition('first');
            this._full_redrawing = false;
            full_redraw = true;
            this.changedDepthMethod('norender');
         }

         if (this._first_drawing) {
            this.adjustCameraPosition('first');
            this.showDrawInfo();
            this._first_drawing = false;
            first_time = true;
            full_redraw = true;
         }

         if (first_time)
            this.completeScene();

         if (full_redraw && (this.ctrl.trans_radial || this.ctrl.trans_z))
            this.changedTransformation('norender');

         if (full_redraw)
            return this.drawAxesAndOverlay(true);
      }).then(() => {
         this._scene.overrideMaterial = null;

         if (this._provided_more_nodes !== undefined) {
            this.appendMoreNodes(this._provided_more_nodes, true);
            delete this._provided_more_nodes;
         }

         if (check_extras) {
            // if extra object where append, redraw them at the end
            this.getExtrasContainer('delete'); // delete old container
            const extras = this._main_painter?._extraObjects || this._extraObjects;
            return this.drawExtras(extras, '', false);
         }
      }).then(() => {
         this.updateClipping(true); // do not render

         this.render3D(0, true);

         if (close_progress) showProgress();

         this.addOrbitControls();

         if (first_time && !this.isBatchMode()) {
            // after first draw check if highlight can be enabled
            if (this.ctrl.highlight === 0)
               this.ctrl.highlight = (this.first_render_tm < 1000);

            // also highlight of scene object can be assigned at the first draw
            if (this.ctrl.highlight_scene === 0)
               this.ctrl.highlight_scene = this.ctrl.highlight;

            // if rotation was enabled, do it
            if (this._webgl && this.ctrl.rotate && !this.ctrl.project) this.autorotate(2.5);
            if (this._webgl && this.ctrl.show_controls) this.showControlGui(true);
         }

         this.setAsMainPainter();

         if (isFunc(this._resolveFunc)) {
            this._resolveFunc(this);
            delete this._resolveFunc;
         }

         if (isFunc(this._complete_handler))
            this._complete_handler(this);

         if (this._draw_nodes_again)
            this.startDrawGeometry(); // relaunch drawing
         else
            this._drawing_ready = true; // indicate that drawing is completed

         return this;
      });
   }

   /** @summary Returns true if geometry drawing is completed */
   isDrawingReady() {
      return this._drawing_ready || false;
   }

   /** @summary Remove already drawn node. Used by geom viewer */
   removeDrawnNode(nodeid) {
      if (!this._draw_nodes) return;

      const new_nodes = [];

      for (let n = 0; n < this._draw_nodes.length; ++n) {
         const entry = this._draw_nodes[n];
         if ((entry.nodeid === nodeid) || this._clones.isIdInStack(nodeid, entry.stack))
            this._clones.createObject3D(entry.stack, this._toplevel, 'delete_mesh');
          else
            new_nodes.push(entry);
      }

      if (new_nodes.length < this._draw_nodes.length) {
         this._draw_nodes = new_nodes;
         this.render3D();
      }
   }

   /** @summary Cleanup geometry painter */
   cleanup(first_time) {
      if (!first_time) {
         let can3d = 0;

         if (!this.superimpose) {
            this.clearTopPainter(); // remove as pointer

            if (this._on_pad) {
               const fp = this.getFramePainter();
               if (fp?.mode3d) {
                  fp.clear3dCanvas();
                  fp.mode3d = false;
               }
            } else
               can3d = this.clear3dCanvas(); // remove 3d canvas from main HTML element


            disposeThreejsObject(this._scene);
         }

         this._toolbar?.cleanup(); // remove toolbar

         disposeThreejsObject(this._full_geom);

         this._controls?.cleanup();

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

         this._gui?.destroy();

         this._worker?.terminate();

         delete this._animating;

         const obj = this.getGeometry();
         if (obj && this.ctrl.is_main) {
            if (obj.$geo_painter === this)
               delete obj.$geo_painter;
            else if (obj.fVolume?.$geo_painter === this)
               delete obj.fVolume.$geo_painter;
         }

         if (this._main_painter?._slave_painters) {
            const pos = this._main_painter._slave_painters.indexOf(this);
            if (pos >= 0) this._main_painter._slave_painters.splice(pos, 1);
         }

         for (let k = 0; k < this._slave_painters?.length; ++k) {
            const slave = this._slave_painters[k];
            if (slave?._main_painter === this) slave._main_painter = null;
         }

         delete this.geo_manager;
         delete this._highlight_handlers;

         super.cleanup();

         delete this.ctrl;
         delete this.options;

         this.did_cleanup = true;

         if (can3d < 0) this.selectDom().html('');
      }

      if (this._slave_painters) {
         for (const k in this._slave_painters) {
            const slave = this._slave_painters[k];
            slave._main_painter = null;
            if (slave._clones === this._clones) slave._clones = null;
         }
      }

      this._main_painter = null;
      this._slave_painters = [];

      if (this._render_resolveFuncs) {
         this._render_resolveFuncs.forEach(func => func(this));
         delete this._render_resolveFuncs;
      }

      if (!this.superimpose)
         cleanupRender3D(this._renderer);

      this.ensureBloom(false);
      delete this._effectComposer;

      delete this._scene;
      delete this._scene_size;
      this._scene_width = 0;
      this._scene_height = 0;
      this._renderer = null;
      this._toplevel = null;
      delete this._full_geom;
      delete this._fog;
      delete this._camera;
      delete this._camera0pos;
      delete this._lookat;
      delete this._selected_mesh;

      if (this._clones && this._clones_owner)
         this._clones.cleanup(this._draw_nodes, this._build_shapes);
      delete this._clones;
      delete this._clones_owner;
      delete this._draw_nodes;
      delete this._drawing_ready;
      delete this._build_shapes;
      delete this._new_draw_nodes;
      delete this._new_append_nodes;
      delete this._last_camera_position;

      this.first_render_tm = 0; // time needed for first rendering
      this.last_render_tm = 0;

      this.changeStage(stageInit, 'cleanup');
      delete this.drawing_log;

      delete this._gui;
      delete this._controls;
      delete this._context_menu;
      delete this._toolbar;

      delete this._worker;
   }

   /** @summary perform resize */
   performResize(width, height) {
      if ((this._scene_width === width) && (this._scene_height === height)) return false;
      if ((width < 10) || (height < 10)) return false;

      this._scene_width = width;
      this._scene_height = height;

      if (this._camera && this._renderer) {
         if (this._camera.isPerspectiveCamera)
            this._camera.aspect = this._scene_width / this._scene_height;
         else if (this._camera.isOrthographicCamera)
            this.adjustCameraPosition(true, true);
         this._camera.updateProjectionMatrix();
         this._renderer.setSize(this._scene_width, this._scene_height, !this._fit_main_area);
         this._effectComposer?.setSize(this._scene_width, this._scene_height);
         this._bloomComposer?.setSize(this._scene_width, this._scene_height);

         if (this.isStage(stageInit))
            this.render3D();
      }

      return true;
   }

   /** @summary Check if HTML element was resized and drawing need to be adjusted */
   checkResize(arg) {
      const cp = this.getCanvPainter();

      // firefox is the only browser which correctly supports resize of embedded canvas,
      // for others we should force canvas redrawing at every step
      if (cp && !cp.checkCanvasResize(arg)) return false;

      const sz = this.getSizeFor3d();

      return this.performResize(sz.width, sz.height);
   }

   /** @summary Toggle enlarge state */
   toggleEnlarge() {
      if (this.enlargeMain('toggle'))
        this.checkResize();
   }

   /** @summary either change mesh wireframe or return current value
     * @return undefined when wireframe cannot be accessed
     * @private */
   accessObjectWireFrame(obj, on) {
      if (!obj?.material) return;

      if ((on !== undefined) && obj.stack)
         obj.material.wireframe = on;

      return obj.material.wireframe;
   }

   /** @summary handle wireframe flag change in GUI
     * @private */
   changedWireFrame() {
      this._scene?.traverse(obj => this.accessObjectWireFrame(obj, this.ctrl.wireframe));

      this.render3D();
   }

   /** @summary Update object in geo painter */
   updateObject(obj) {
      if ((obj === 'same') || !obj?._typename)
         return false;
      if (obj === this.getObject())
         return true;

      let gm;
      if (obj._typename === clTGeoManager) {
         gm = obj;
         obj = obj.fMasterVolume;
      }

      if (obj._typename.indexOf(clTGeoVolume) === 0)
         obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };

      if (this.geo_manager && gm) {
         this.geo_manager = gm;
         this.assignObject(obj);
         this._did_update = true;
         return true;
      }

      if (!this.matchObjectType(obj._typename))
         return false;

      this.assignObject(obj);
      this._did_update = true;
      return true;
   }

   /** @summary Cleanup TGeo drawings */
   clearDrawings() {
      if (this._clones && this._clones_owner)
         this._clones.cleanup(this._draw_nodes, this._build_shapes);
      delete this._clones;
      delete this._clones_owner;
      delete this._draw_nodes;
      delete this._drawing_ready;
      delete this._build_shapes;

      delete this._extraObjects;
      delete this._clipCfg;

      // only remove all childs from top level object
      disposeThreejsObject(this._toplevel, true);

      this._full_redrawing = true;
   }

    /** @summary Redraw TGeo object inside TPad */
   redraw() {
      if (this.superimpose) {
         const cfg = getHistPainter3DCfg(this.getMainPainter());

         if (cfg) {
            this._toplevel.scale.set(cfg.scale_x ?? 1, cfg.scale_y ?? 1, cfg.scale_z ?? 1);
            this._toplevel.position.set(cfg.offset_x ?? 0, cfg.offset_y ?? 0, cfg.offset_z ?? 0);
            this._toplevel.updateMatrix();
            this._toplevel.updateMatrixWorld();
         }
      }

      if (this._did_update)
         return this.startRedraw();

      const main = this._on_pad ? this.getFramePainter() : null;
      if (!main)
         return Promise.resolve(false);
      const sz = main.getSizeFor3d(main.access3dKind());
      main.apply3dSize(sz);
      return this.performResize(sz.width, sz.height);
   }

   /** @summary Redraw TGeo object */
   redrawObject(obj, opt) {
      if (!this.updateObject(obj, opt))
         return false;

      return this.startRedraw();
   }

   /** @summary Start geometry redraw */
   startRedraw(tmout) {
      if (tmout) {
         if (this._redraw_timer)
            clearTimeout(this._redraw_timer);
         this._redraw_timer = setTimeout(() => this.startRedraw(), tmout);
         return;
      }

      delete this._redraw_timer;
      delete this._did_update;

      this.clearDrawings();
      const draw_obj = this.getGeometry(),
            name_prefix = this.geo_manager ? draw_obj.fName : '';
      return this.prepareObjectDraw(draw_obj, name_prefix);
   }

  /** @summary draw TGeo object */
   static async draw(dom, obj, opt) {
      if (!obj) return null;

      let shape = null, extras = null, extras_path = '', is_eve = false;

      if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
         shape = obj; obj = null;
      } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
         shape = obj.fShape;
       else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
         shape = obj.fShape; is_eve = true;
      } else if (obj._typename === clTGeoManager)
         shape = obj.fMasterVolume.fShape;
       else if (obj._typename === clTGeoOverlap) {
         extras = obj.fMarker; extras_path = '<prnt>/Marker';
         obj = buildOverlapVolume(obj);
         if (!opt) opt = 'wire';
      } else if ('fVolume' in obj) {
         if (obj.fVolume) shape = obj.fVolume.fShape;
      } else
         obj = null;


      if (isStr(opt) && opt.indexOf('comp') === 0 && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
         let maxlvl = 1;
         opt = opt.slice(4);
         if (opt[0] === 'x') { maxlvl = 999; opt = opt.slice(1) + '_vislvl999'; }
         obj = buildCompositeVolume(shape, maxlvl);
      }

      if (!obj && shape) {
         obj = Object.assign(create(clTNamed),
                   { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });
      }

      if (!obj) return null;

      const painter = createGeoPainter(dom, obj, opt);

      if (painter.ctrl.is_main && !obj.$geo_painter)
         obj.$geo_painter = painter;

      if (!painter.ctrl.is_main && painter.ctrl.project && obj.$geo_painter) {
         painter._main_painter = obj.$geo_painter;
         painter._main_painter._slave_painters.push(painter);
      }

      if (is_eve && (!painter.ctrl.vislevel || (painter.ctrl.vislevel < 9)))
         painter.ctrl.vislevel = 9;

      if (extras) {
         painter._splitColors = true;
         painter.addExtra(extras, extras_path);
      }

      return painter.loadMacro(painter.ctrl.script_name).then(arg => painter.prepareObjectDraw(arg.obj, arg.prefix));
   }

} // class TGeoPainter


let add_settings = false;

/** @summary Create geo-related css entries
  * @private */
function injectGeoStyle() {
   if (!add_settings && isFunc(internals.addDrawFunc)) {
      add_settings = true;
      // indication that draw and hierarchy is loaded, create css
      internals.addDrawFunc({ name: clTEvePointSet, icon_get: getBrowserIcon, icon_click: browserIconClick });
      internals.addDrawFunc({ name: clTEveTrack, icon_get: getBrowserIcon, icon_click: browserIconClick });
   }

   function img(name, code) {
      return `.jsroot .img_${name} { display: inline-block; height: 16px; width: 16px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQ${code}'); }`;
   }

   injectStyle(`
${img('geoarb8', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB1SURBVBjTdY6rEYAwEETTy6lzK8/Fo+Jj18dTAjUgaQGfGiggtRDE8RtY93Zu514If2nzk2ux9c5TZkwXbiWTUavzws69oBfpYBrMT4r0Jhsw+QfRgQSw+CaKRsKsnV+SaF8MN49RBSgPUxO85PMl5n4tfGUH2gghs2uPAeQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geocombi', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAlUlEQVQoz5VQMQ4CMQyzEUNnBqT7Bo+4nZUH8gj+welWJsQDkHoCEYakTXMHSFiq2jqu4xRAEl2A7w4myWzpzCSZRZ658ldKu1hPnFsequBIc/hcLli3l52MAIANtpWrDsv8waGTW6BPuFtsdZArXyFuj33TQpazGEQF38phipnLgItxRcAoOeNpzv4PTXnC42fb//AGI5YqfQAU8dkAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geocone', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACRSURBVBjTdY+xDcNACEVvEm/ggo6Olva37IB0C3iEzJABvAHFTXBDeJRwthMnUvylk44vPjxK+afeokX0flQhJO7L4pafSOMxzaxIKc/Tc7SIjNLyieyZSjBzc4DqMZI0HTMonWPBNlogOLeuewbg9c0hOiIqH7DKmTCuFykjHe4XOzQ58XVMGxzt575tKzd6AX9yMkcWyPlsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geogtra', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACCSURBVBjTVc+hDQMxDAVQD1FyqCQk0MwsCwQEG3+eCW6B0FvheDboFMGepTlVitPP/Cz5y0S/mNkw8pySU9INJDDH4vM4Usm5OrQXasXtkA+tQF+zxfcDY8EVwgNeiwmA37TEccK5oLOwQtuCj7BM2Fq7iGrxVqJbSsH+GzXs+798AThwKMh3/6jDAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geomedium', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABVQTFRFAAAAAAAAMDAww8PDWKj/////gICAG0/C4AAAAAF0Uk5TAEDm2GYAAAABYktHRAX4b+nHAAAACXBIWXMAAABIAAAASABGyWs+AAAAXElEQVQI102MwRGAMAgEuQ6IDwvQCjQdhAl/H7ED038JHhkd3dcOLAgESFARaAqnEB3yrj6QSEym1RbbOKinN+8q2Esui1GaX7VXSi4RUbxHRbER8X6O5Pg/fLgBBzMN8HfXD3AAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geopara', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABtSURBVBjTY2DADq5MT7+CzD9kaKjp+QhJYIWqublhMbKAgpOnZxWSQJdsVJTndCSBKoWoAM/VSALpqlEBAYeQBKJAAsi2BGgCBZDdEWUYFZCOLFBlGOWJ7AyGFeaotjIccopageK3R12PGHABACTYHWd0tGw6AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('georotation', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAiklEQVQoz2NgYGBgYGDg+A/BmIAFIvyDEbs0AwMTAwHACLPiB5QVBTdpGSOSCZjScDcgc4z+32BgYGBgEGIQw3QDLkdCTZD8/xJFeBfDVxQT/j9n/MeIrMCNIRBJwX8GRuzGM/yHKMAljeILNFOuMTyEisEUMKIqucrwB2oyIhyQpH8y/MZrLWkAAHFzIHIc0Q5yAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotranslation', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABESURBVBjTY2DgYGAAYzjgAAIQgSLAgSwAAcrWUUCAJBAVhSpgBAQumALGCJPAAsriHIS0IAQ4UAU4cGphQBWwZSAOAADGJBKdZk/rHQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geotrd2', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABsSURBVBjTbY+xDcAwCARZx6UraiaAmpoRvIIb75PWI2QITxIiRQKk0CCO/xcA/NZ9LRs7RkJEYg3QxczUwoGsXiMAoe8lAelqRWFNKpiNXZLAalRDd0f3TMgeMckABKsCDmu+442RddeHz9cf9jUkW8smGn8AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geovolume', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAB5QTFRFAAAAMDAw///Ay8uc/7+Q/4BgmJh4gIDgAAD/////CZb2ugAAAAF0Uk5TAEDm2GYAAAABYktHRAnx2aXsAAAACXBIWXMAAABIAAAASABGyWs+AAAAR0lEQVQI12NggAEBIBAEQgYGQUYQAyIGIhgwAZMSGCgwMJuEKimFOhswsKWAGG4JDGxJIBk1EEO9o6NIDVkEpgauC24ODAAASQ8Pkj/retYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoassembly', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAA9QTFRFAAAAMDAw/wAAAAD/////jEo0BQAAAAF0Uk5TAEDm2GYAAAABYktHRASPaNlRAAAACXBIWXMAAABIAAAASABGyWs+AAAAOklEQVQI12NggAFGRgEgEBRgEBSAMhgYGQQEgAR+oARGDIwCIAYjUL0A2DQQg9nY2ABVBKoGrgsDAADxzgNboMz8zQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geocomposite', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABuSURBVBjTY2AgF2hqgQCCr+0V4O7hFmgCF7CJyKysKkmxhfGNLaw9SppqAi2gfMuY5Agrl+ZaC6iAUXRJZX6Ic0klTMA5urapPFY5NRcmYKFqWl8S5RobBRNg0PbNT3a1dDGH8RlM3LysTRjIBwAG6xrzJt11BAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geoctub', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACESURBVBjTdc+xDcMwDARA7cKKHTuWX37LHaw+vQbQAJomA7j2DB7FhCMFCZB8pxPwJEv5kQcZW+3HencRBekak4aaMQIi8YJdAQ1CMeE0UBkuaLMETklQ9Alhka0JzzXWqLVBuQYPpWcVuBbZjZafNRYcDk9o/b07bvhINz+/zxu1/M0FSRcmAk/HaIcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geohype', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACKSURBVBjTbU+rFQQhDKQSDDISEYuMREfHx6eHKMpYuf5qoIQt5bgDblfcuJk3nySEhSvceDV3c/ejT66lspopE9pXyIlkCrHMBACpu1DClekQAREi/loviCnF/NhRwJLaQ6hVhPjB8bOCsjlnNnNl0FWJVWxAqGzHONRHpu5Ml+nQ+8GzNW9n+Is3eg80Nk0iiwoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geomixture', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAACFQTFRFAAAAAAAAKysrVVUA//8B//8AgICAqqpV398gv79A////VYJtlwAAAAF0Uk5TAEDm2GYAAAABYktHRApo0PRWAAAACXBIWXMAAABIAAAASABGyWs+AAAAXklEQVQI12NgwASCQsJCgoZAhoADq1tKIJAhEpDGxpYIZKgxsLElgBhibAkOCY4gKTaGkPRGIEPUIYEBrEaAIY0tDawmgYWNgREkkjCVjRWkWCUhLY0FJCIIBljsBgCZTAykgaRiRwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geopcon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACJSURBVBjTdc+hGcQwCIZhhjl/rkgWiECj8XgGyAbZoD5LdIRMkEnKkV575n75Pp8AgLU54dmh6mauelyAL2Qzxfe2sklioq6FacFAcRFXYhwJHdU5rDD2hEYB/CmoJVRMiIJqgtENuoqA8ltAlYAqRH4d1tGkwzTqN2gA7Nv+fUwkgZ/3mg34txM+szzATJS1HQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geosphere', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACFSURBVBjTdY+xEcQwCAQp5QNFjpQ5vZACFBFTADFFfKYCXINzlUAJruXll2ekxDAEt9zcANFbXb2mqm56dxsymAH0yccAJaeNi0h5QGyfxGJmivMPjj0nmLsbRmyFCss3rlbpcUjfS8wLUNRcJyCF6uqg2IvYCnoKC7f1kSbA6riTz7evfwj3Ml+H3KBqAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotrap', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB5SURBVBjTbY+hFYAwDETZB1OJi4yNPp0JqjtAZ2AELL5DdABmIS2PtLxHXH7u7l2W5W+uHMHpGiCHLYR1yw4SCZMIXBOJWVSjK7QDDAu4g8OBmAKK4sAEDdR3rw8YmcUcrEijKKhl7lN1IQPn9ExlgU6/WEyc75+5AYK0KY5oHBDfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotubeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACBSURBVBjTdc+hEcQwDARA12P6QFBQ9LDwcXEVkA7SQTr4BlJBakgpsWdsh/wfux3NSCrlV86Mlrxmz1pBWq3bAHwETohxABVmDZADQp1BE+wDNnGywzHgmHDOreJNTDH3Xn3CVX0dpu2MHcIFBkYp/gKsQ8SCQ72V+36/+2aWf3kAQfgshnpXF0wAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoxtru', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABcSURBVBjTY2AgEmhpeZV56vmWwQW00QUYwAJlSAI6XmVqukh8PT1bT03PchhXX09Pr9wQIQDiJ+ZowgWAXD3bck+QQDlCQTkDQgCoxA/ERBKwhbDglgA1lDMQDwCc/Rvq8nYsWgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geobbox', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTVc+hEYAwDAXQLlNRF1tVGxn9NRswQiSSCdgDyQBM0FlIIb2WuL77uf6E8E0N02wKYRwDciTKREVvB04GuZSyOMCABRB1WGzF3uDNQTvs/RcDtJXT4fSEXA5XoiQt0ttVSm8Co2psIOvoimjAOqBmFtH5wEP2373TPIvTK1nrpULXAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geoconeseg', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB4SURBVBjTdc6hEcAgDAXQbFNZXHQkFlkd/30myAIMwAws0gmYpVzvoFyv/S5P/B+izzQ387ZA2pkDnvsU1SQLVIFrOM4JFmEaYp2gCQbmPEGODhJ8jt7Am47hwgrzInGAifa/elUZnQLY00iU30BZAV+BWi2VfnIBv1osbHH8jX0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMTItMDJUMTQ6MjY6MjkrMDE6MDDARtd2AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTExLTEyVDA4OjM5OjE5KzAxOjAwO3ydwwAAAABJRU5ErkJggg==')}
${img('geoeltu', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTdY+hFYUwDEU7xq9CIXC4uNjY6KczQXeoYgVMR2ABRmCGjvIp/6dgiEruueedvBDuOR57LQnKyc8CJmKO+N8bieIUPtmBWjIIx8XDBHYCipsnql1g2D0UP2OoDqwBncf+RdZmzFMHizRjog7KZYzawd4Ay93lEAPWR7WAvNbwMl/XwSxBV8qCjgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geomaterial', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAbElEQVQoz62QMRbAIAhDP319Xon7j54qHSyCtaMZFCUkRjgDIdRU9yZUCfg8ut5aAHdcxtoNurmgA3ABNKIR9KimhSukPe2qxcCYC0pfFXx/aFWo7i42KKItOpopqvvnLzJmtlZTS7EfGAfwAM4EQbLIGV0sAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geoparab', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbY+xDYAwDAQ9UAp3X7p0m9o9dUZgA9oMwAjpMwMzMAnYBAQSX9mn9+tN9KOtzsWsLOvYCziUGNX3nnCLJRzKPgeYrhPW7FJNLUB3YJazYKQKTnBaxgXRzNmJcrt7XCHQp9kEB1wfELEir/KGj4Foh8A+/zW1nf51AFabKZuWK+mNAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geopgon', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAABwSURBVBjTY2AgDlwAAzh3sX1sPRDEeuwDc+8V2dsHgQQ8LCzq74HkLSzs7Yva2tLt7S3sN4MNiDUGKQmysCi6BzWkzcI+PdY+aDPCljZlj1iFOUjW1tvHLjYuQhJIt5/DcAFZYLH9YnSn7iPST9gAACbsJth21haFAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotorus', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTjY+hFcMwDEQ9SkFggXGIoejhw+LiGkBDlHoAr+AhgjNL5byChuXeE7gvPelUyjOds/f5Zw0ggfj5KVCPMBWeyx+SbQ1XUriAC2XfpWWxjQQEZasRtRHiCUAj3qN4JaolUJppzh4q7dUTdHFXW/tH9OuswWm3nI7tc08+/eGLl758ey9KpKrNOQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('geotrd1', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAAB/SURBVBjTbc6xDQMhDAVQ9qH6lUtal65/zQ5IDMAMmYAZrmKGm4FJzlEQQUo+bvwkG4fwm9lbodV7w40Y4WGfSxQiXiJlQfZOjWRb8Ioi3tKuBQMCo7+9N72BzPsfAuoTdUP9QN8wgOQwvsfWmHzpeT5BKydMNW0nhJGvGf7mAc5WKO9e5N2dAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTEyLTAyVDE0OjI2OjI5KzAxOjAwwEbXdgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNC0xMS0xMlQwODozOToxOSswMTowMDt8ncMAAAAASUVORK5CYII=')}
${img('geotube', 'CAAAAAA6mKC9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJ0Uk5TAAB2k804AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AAACGSURBVBjTRc+tEcAwCAXgLFNbWeSzSDQazw5doWNUZIOM0BEyS/NHy10E30HyklKvWnJ+0le3sJoKn3X2z7GRuvG++YRyMMDt0IIKUXMzxbnugJi5m9K1gNnGBOUFElAWGMaKIKI4xoQggl00gT+A9hXWgDwnfqgsHRAx2m+8bfjfdyrx5AtsSjpwu+M2RgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0xMi0wMlQxNDoyNjoyOSswMTowMMBG13YAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTQtMTEtMTJUMDg6Mzk6MTkrMDE6MDA7fJ3DAAAAAElFTkSuQmCC')}
${img('evepoints', 'BAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABJQTFRF////n4mJcEdKRDMzcEdH////lLE/CwAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAABIAAAASABGyWs+AAAAI0lEQVQI12NgIAowIpgKEJIZLiAgAKWZGQzQ9UGlWIizBQgAN4IAvGtVrTcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDktMDJUMTU6MDQ6MzgrMDI6MDDPyc7hAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTA5LTAyVDE1OjA0OjM4KzAyOjAwvpR2XQAAAABJRU5ErkJggg==')}
${img('evetrack', 'CAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAABIAAAASABGyWs+AAAAqElEQVQoz32RMQrCQBBFf4IgSMB0IpGkMpVHCFh7BbHIGTyVhU0K8QYewEKsbVJZaCUiPAsXV8Puzhaz7H8zs5+JUDjikLilQr5zpCRl5xMXZNScQE5gSMGaz70jjUAJcw5c3UBMTsUe+9Kzf065SbropeLXimWfDIgoab/tOyPGzOhz53+oSWcSGh7UdB2ZNKXBZdgAuUdEKJYmrEILyVgG6pE2tEHgDfe42rbjYzSHAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA5LTAyVDE1OjA0OjQ3KzAyOjAwM0S3EQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wOS0wMlQxNTowNDo0NyswMjowMEIZD60AAAAASUVORK5CYII=')}
.jsroot .geovis_this { background-color: lightgreen; }
.jsroot .geovis_daughters { background-color: lightblue; }
.jsroot .geovis_all { background-color: yellow; }`);
}


/** @summary Create geo painter
  * @private */
function createGeoPainter(dom, obj, opt) {
   injectGeoStyle();

   geoCfg('GradPerSegm', settings.GeoGradPerSegm);
   geoCfg('CompressComp', settings.GeoCompressComp);

   const painter = new TGeoPainter(dom, obj);

   painter.decodeOptions(opt); // indicator of initialization

   return painter;
}


/** @summary provide menu for geo object
  * @private */
function provideMenu(menu, item, hpainter) {
   if (!item._geoobj) return false;

   const obj = item._geoobj, vol = item._volume,
         iseve = ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract));

   if (!vol && !iseve) return false;

   menu.add('separator');

   const ScanEveVisible = (obj, arg, skip_this) => {
      if (!arg) arg = { visible: 0, hidden: 0 };

      if (!skip_this) {
         if (arg.assign !== undefined)
            obj.fRnrSelf = arg.assign;
         else if (obj.fRnrSelf)
            arg.vis++;
         else
            arg.hidden++;
      }

      if (obj.fElements) {
         for (let n = 0; n < obj.fElements.arr.length; ++n)
            ScanEveVisible(obj.fElements.arr[n], arg, false);
      }

      return arg;
   }, ToggleEveVisibility = arg => {
      if (arg === 'self') {
         obj.fRnrSelf = !obj.fRnrSelf;
         item._icon = item._icon.split(' ')[0] + provideVisStyle(obj);
         hpainter.updateTreeNode(item);
      } else {
         ScanEveVisible(obj, { assign: (arg === 'true') }, true);
         hpainter.forEachItem(m => {
            // update all child items
            if (m._geoobj && m._icon) {
               m._icon = item._icon.split(' ')[0] + provideVisStyle(m._geoobj);
               hpainter.updateTreeNode(m);
            }
         }, item);
      }

      findItemWithPainter(item, 'testGeomChanges');
   }, ToggleMenuBit = arg => {
      toggleGeoBit(vol, arg);
      const newname = item._icon.split(' ')[0] + provideVisStyle(vol);
      hpainter.forEachItem(m => {
         // update all items with that volume
         if (item._volume === m._volume) {
            m._icon = newname;
            hpainter.updateTreeNode(m);
         }
      });

      hpainter.updateTreeNode(item);
      findItemWithPainter(item, 'testGeomChanges');
   },

    drawitem = findItemWithPainter(item),
       fullname = drawitem ? hpainter.itemFullName(item, drawitem) : '';

   if ((item._geoobj._typename.indexOf(clTGeoNode) === 0) && drawitem) {
      menu.add('Focus', () => {
        if (drawitem && isFunc(drawitem._painter?.focusOnItem))
           drawitem._painter.focusOnItem(fullname);
      });
   }

   if (iseve) {
      menu.addchk(obj.fRnrSelf, 'Visible', 'self', ToggleEveVisibility);
      const res = ScanEveVisible(obj, undefined, true);
      if (res.hidden + res.visible > 0)
         menu.addchk((res.hidden === 0), 'Daughters', res.hidden !== 0 ? 'true' : 'false', ToggleEveVisibility);
   } else {
      const stack = drawitem?._painter?._clones?.findStackByName(fullname),
          phys_vis = stack ? drawitem._painter._clones.getPhysNodeVisibility(stack) : null,
          is_visible = testGeoBit(vol, geoBITS.kVisThis);

      menu.addchk(testGeoBit(vol, geoBITS.kVisNone), 'Invisible',
            geoBITS.kVisNone, ToggleMenuBit);
      if (stack) {
         const changePhysVis = arg => {
            drawitem._painter._clones.setPhysNodeVisibility(stack, (arg === 'off') ? false : arg);
            findItemWithPainter(item, 'testGeomChanges');
         };

         menu.add('sub:Physical vis', 'Physical node visibility - only for this instance');
         menu.addchk(phys_vis?.visible, 'on', 'on', changePhysVis, 'Enable visibility of phys node');
         menu.addchk(phys_vis && !phys_vis.visible, 'off', 'off', changePhysVis, 'Disable visibility of physical node');
         menu.add('reset', 'clear', changePhysVis, 'Reset custom visibility of physical node');
         menu.add('reset all', 'clearall', changePhysVis, 'Reset all custom settings for all nodes');
         menu.add('endsub:');
      }

      menu.addchk(is_visible, 'Logical vis',
            geoBITS.kVisThis, ToggleMenuBit, 'Logical node visibility - all instances');
      menu.addchk(testGeoBit(vol, geoBITS.kVisDaughters), 'Daughters',
            geoBITS.kVisDaughters, ToggleMenuBit, 'Logical node daugthers visibility');
   }

   return true;
}

/** @summary handle click on browser icon
  * @private */
function browserIconClick(hitem, hpainter) {
   if (hitem._volume) {
      if (hitem._more && hitem._volume.fNodes?.arr?.length)
         toggleGeoBit(hitem._volume, geoBITS.kVisDaughters);
      else
         toggleGeoBit(hitem._volume, geoBITS.kVisThis);

      updateBrowserIcons(hitem._volume, hpainter);

      findItemWithPainter(hitem, 'testGeomChanges');
      return false; // no need to update icon - we did it ourself
   }

   if (hitem._geoobj && ((hitem._geoobj._typename === clTEveGeoShapeExtract) || (hitem._geoobj._typename === clREveGeoShapeExtract))) {
      hitem._geoobj.fRnrSelf = !hitem._geoobj.fRnrSelf;

      updateBrowserIcons(hitem._geoobj, hpainter);
      findItemWithPainter(hitem, 'testGeomChanges');
      return false; // no need to update icon - we did it ourself
   }

   // first check that geo painter assigned with the item
   const drawitem = findItemWithPainter(hitem),
       newstate = drawitem?._painter?.extraObjectVisible(hpainter, hitem, true);

   // return true means browser should update icon for the item
   return newstate !== undefined;
}


/** @summary Get icon for the browser
  * @private */
function getBrowserIcon(hitem, hpainter) {
   let icon = '';
   switch (hitem._kind) {
      case prROOT + clTEveTrack: icon = 'img_evetrack'; break;
      case prROOT + clTEvePointSet: icon = 'img_evepoints'; break;
      case prROOT + clTPolyMarker3D: icon = 'img_evepoints'; break;
   }
   if (icon) {
      const drawitem = findItemWithPainter(hitem);
      if (drawitem?._painter?.extraObjectVisible(hpainter, hitem))
         icon += ' geovis_this';
   }
   return icon;
}


/** @summary create hierarchy item for geo object
  * @private */
function createItem(node, obj, name) {
   const sub = {
      _kind: prROOT + obj._typename,
      _name: name || getObjectName(obj),
      _title: obj.fTitle,
      _parent: node,
      _geoobj: obj,
      _get(item /* ,itemname */) {
          // mark object as belong to the hierarchy, require to
          if (item._geoobj) item._geoobj.$geoh = true;
          return Promise.resolve(item._geoobj);
      }
   };
   let volume, shape, subnodes, iseve = false;

   if (obj._typename === 'TGeoMaterial')
      sub._icon = 'img_geomaterial';
   else if (obj._typename === 'TGeoMedium')
      sub._icon = 'img_geomedium';
   else if (obj._typename === 'TGeoMixture')
      sub._icon = 'img_geomixture';
   else if ((obj._typename.indexOf(clTGeoNode) === 0) && obj.fVolume) {
      sub._title = 'node:' + obj._typename;
      if (obj.fTitle) sub._title += ' ' + obj.fTitle;
      volume = obj.fVolume;
   } else if (obj._typename.indexOf(clTGeoVolume) === 0)
      volume = obj;
   else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract)) {
      iseve = true;
      shape = obj.fShape;
      subnodes = obj.fElements ? obj.fElements.arr : null;
   } else if ((obj.fShapeBits !== undefined) && (obj.fShapeId !== undefined))
      shape = obj;

   if (volume) {
      shape = volume.fShape;
      subnodes = volume.fNodes ? volume.fNodes.arr : null;
   }

   if (volume || shape || subnodes) {
      if (volume) sub._volume = volume;

      if (subnodes) {
         sub._more = true;
         sub._expand = expandGeoObject;
      } else if (shape && (shape._typename === clTGeoCompositeShape) && shape.fNode) {
         sub._more = true;
         sub._shape = shape;
         sub._expand = function(node /*, obj */) {
            createItem(node, node._shape.fNode.fLeft, 'Left');
            createItem(node, node._shape.fNode.fRight, 'Right');
            return true;
         };
      }

      if (!sub._title && (obj._typename !== clTGeoVolume))
         sub._title = obj._typename;

      if (shape) {
         if (sub._title === '')
            sub._title = shape._typename;

         sub._icon = getShapeIcon(shape);
      } else
         sub._icon = sub._more ? 'img_geocombi' : 'img_geobbox';

      if (volume)
         sub._icon += provideVisStyle(volume);
      else if (iseve)
         sub._icon += provideVisStyle(obj);

      sub._menu = provideMenu;
      sub._icon_click = browserIconClick;
   }

   if (!node._childs) node._childs = [];

   if (!sub._name) {
      if (isStr(node._name)) {
         sub._name = node._name;
         if (sub._name.lastIndexOf('s') === sub._name.length-1)
            sub._name = sub._name.slice(0, sub._name.length-1);
         sub._name += '_' + node._childs.length;
      } else
         sub._name = 'item_' + node._childs.length;
   }

   node._childs.push(sub);

   return sub;
}

/** @summary Draw dummy geometry
  * @private */
async function drawDummy3DGeom(painter) {
   const shape = create(clTNamed);
   shape._typename = clTGeoBBox;
   shape.fDX = 1e-10;
   shape.fDY = 1e-10;
   shape.fDZ = 1e-10;
   shape.fShapeId = 1;
   shape.fShapeBits = 0;
   shape.fOrigin = [0, 0, 0];

   const obj = Object.assign(create(clTNamed),
                { _typename: clTEveGeoShapeExtract,
                  fTrans: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
                  fShape: shape, fRGBA: [0, 0, 0, 0], fElements: null, fRnrSelf: false }),
         pp = painter.getPadPainter(),
         opt = (pp?.pad?.fFillColor && (pp?.pad?.fFillStyle > 1000)) ? 'bkgr_' + pp.pad.fFillColor : '';

   return TGeoPainter.draw(painter.getDom(), obj, opt)
                     .then(geop => { geop._dummy = true; return geop; });
}

/** @summary Direct draw function for TAxis3D
  * @private */
function drawAxis3D() {
   const main = this.getMainPainter();

   if (isFunc(main?.setAxesDraw))
      return main.setAxesDraw(true);

   console.error('no geometry painter found to toggle TAxis3D drawing');
}

/** @summary Build three.js model for given geometry object
  * @param {Object} obj - TGeo-related object
  * @param {Object} [opt] - options
  * @param {Number} [opt.vislevel] - visibility level like TGeoManager, when not specified - show all
  * @param {Number} [opt.numnodes=1000] - maximal number of visible nodes
  * @param {Number} [opt.numfaces=100000] - approx maximal number of created triangles
  * @param {Number} [opt.instancing=-1] - <0 disable use of InstancedMesh, =0 only for large geometries, >0 enforce usage of InstancedMesh
  * @param {boolean} [opt.doubleside=false] - use double-side material
  * @param {boolean} [opt.wireframe=false] - show wireframe for created shapes
  * @param {boolean} [opt.transparency=0] - make nodes transparent
  * @param {boolean} [opt.dflt_colors=false] - use default ROOT colors
  * @return {object} Object3D with created model
  * @example
  * import { build } from 'https://root.cern/js/latest/modules/geom/TGeoPainter.mjs';
  * let obj3d = build(obj);
  * // this is three.js object and can be now inserted in the scene
  */
function build(obj, opt) {
   if (!obj) return null;

   if (!opt) opt = {};
   if (!opt.numfaces) opt.numfaces = 100000;
   if (!opt.numnodes) opt.numnodes = 1000;
   if (!opt.frustum) opt.frustum = null;

   opt.res_mesh = opt.res_faces = 0;

   if (opt.instancing === undefined)
      opt.instancing = -1;

   opt.info = { num_meshes: 0, num_faces: 0 };

   let clones = null, visibles = null;

   if (obj.visibles && obj.nodes && obj.numnodes) {
      // case of draw message from geometry viewer

      const nodes = obj.numnodes > 1e6 ? { length: obj.numnodes } : new Array(obj.numnodes);

      obj.nodes.forEach(node => {
         nodes[node.id] = ClonedNodes.formatServerElement(node);
      });

      clones = new ClonedNodes(null, nodes);
      clones.name_prefix = clones.getNodeName(0);

      // normally only need when making selection, not used in geo viewer
      // this.geo_clones.setMaxVisNodes(draw_msg.maxvisnodes);
      // this.geo_clones.setVisLevel(draw_msg.vislevel);
      // TODO: provide from server
      clones.maxdepth = 20;

      const nsegm = obj.cfg?.nsegm || 30;

      for (let cnt = 0; cnt < obj.visibles.length; ++cnt) {
         const item = obj.visibles[cnt], rd = item.ri;

         // entry may be provided without shape - it is ok
         if (rd)
            item.server_shape = rd.server_shape = createServerGeometry(rd, nsegm);
      }

      visibles = obj.visibles;
   } else {
      let shape = null, hide_top = false;

      if (('fShapeBits' in obj) && ('fShapeId' in obj)) {
         shape = obj; obj = null;
      } else if ((obj._typename === clTGeoVolumeAssembly) || (obj._typename === clTGeoVolume))
         shape = obj.fShape;
       else if ((obj._typename === clTEveGeoShapeExtract) || (obj._typename === clREveGeoShapeExtract))
         shape = obj.fShape;
       else if (obj._typename === clTGeoManager) {
         obj = obj.fMasterVolume;
         hide_top = !opt.showtop;
         shape = obj.fShape;
      } else if (obj.fVolume)
         shape = obj.fVolume.fShape;
       else
         obj = null;


      if (opt.composite && shape && (shape._typename === clTGeoCompositeShape) && shape.fNode)
         obj = buildCompositeVolume(shape);

      if (!obj && shape)
         obj = Object.assign(create(clTNamed), { _typename: clTEveGeoShapeExtract, fTrans: null, fShape: shape, fRGBA: [0, 1, 0, 1], fElements: null, fRnrSelf: true });

      if (!obj) return null;

      if (obj._typename.indexOf(clTGeoVolume) === 0)
         obj = { _typename: clTGeoNode, fVolume: obj, fName: obj.fName, $geoh: obj.$geoh, _proxy: true };

      clones = new ClonedNodes(obj);
      clones.setVisLevel(opt.vislevel);
      clones.setMaxVisNodes(opt.numnodes);

      if (opt.dflt_colors)
         clones.setDefaultColors(true);

      const uniquevis = opt.no_screen ? 0 : clones.markVisibles(true);
      if (uniquevis <= 0)
         clones.markVisibles(false, false, hide_top);
      else
         clones.markVisibles(true, true, hide_top); // copy bits once and use normal visibility bits

      clones.produceIdShifts();

      // collect visible nodes
      const res = clones.collectVisibles(opt.numfaces, opt.frustum);

      visibles = res.lst;
   }

   if (!opt.material_kind)
      opt.material_kind = 'lambert';

   clones.setConfig(opt);

   // collect shapes
   const shapes = clones.collectShapes(visibles);

   clones.buildShapes(shapes, opt.numfaces);

   const toplevel = new Object3D();
   toplevel.clones = clones; // keep reference on JSROOT data

   const colors = getRootColors();

   if (clones.createInstancedMeshes(opt, toplevel, visibles, shapes, colors))
      return toplevel;

   for (let n = 0; n < visibles.length; ++n) {
      const entry = visibles[n];
      if (entry.done) continue;

      const shape = entry.server_shape || shapes[entry.shapeid];
      if (!shape.ready) {
         console.warn('shape marked as not ready when should');
         break;
      }

      const mesh = clones.createEntryMesh(opt, toplevel, entry, shape, colors);

      if (mesh)
         mesh.name = clones.getNodeName(entry.nodeid);
   }

   return toplevel;
}

export { ClonedNodes, build, TGeoPainter, GeoDrawingControl,
         expandGeoObject, createGeoPainter, drawAxis3D, drawDummy3DGeom, produceRenderOrder };