gpad/TPadPainter.mjs

import { gStyle, settings, constants, browser, internals, BIT,
         create, toJSON, isBatchMode, loadModules, loadScript, injectCode, isPromise, getPromise, postponePromise,
         isObject, isFunc, isStr, clTObjArray, clTPaveText, clTColor, clTPad, clTCanvas, clTFrame, clTStyle, clTLegend,
         clTHStack, clTMultiGraph, clTLegendEntry, nsSVG, kTitle, clTList, urlClassPrefix } from '../core.mjs';
import { select as d3_select, rgb as d3_rgb } from '../d3.mjs';
import { ColorPalette, adoptRootColors, getColorPalette, getGrayColors, extendRootColors,
         getRGBfromTColor, decodeWebCanvasColors } from '../base/colors.mjs';
import { prSVG, prJSON, getElementRect, getAbsPosInCanvas, DrawOptions, compressSVG, makeTranslate,
         getTDatime, convertDate, svgToImage } from '../base/BasePainter.mjs';
import { ObjectPainter, selectActivePad, getActivePad } from '../base/ObjectPainter.mjs';
import { TAttLineHandler } from '../base/TAttLineHandler.mjs';
import { addCustomFont } from '../base/FontHandler.mjs';
import { addDragHandler } from './TFramePainter.mjs';
import { createMenu, closeMenu } from '../gui/menu.mjs';
import { ToolbarIcons, registerForResize, saveFile } from '../gui/utils.mjs';
import { BrowserLayout, getHPainter } from '../gui/display.mjs';


const clTButton = 'TButton', kIsGrayscale = BIT(22);

function getButtonSize(handler, fact) {
   return Math.round((fact || 1) * (handler.iscan || !handler.has_canvas ? 16 : 12));
}

function isPadPainter(p) {
   return p?.pad && isFunc(p.forEachPainterInPad);
}

function toggleButtonsVisibility(handler, action, evnt) {
   evnt?.preventDefault();
   evnt?.stopPropagation();

   const group = handler.getLayerSvg('btns_layer', handler.this_pad_name),
         btn = group.select('[name=\'Toggle\']');

   if (btn.empty()) return;

   let state = btn.property('buttons_state');

   if (btn.property('timout_handler')) {
      if (action !== 'timeout') clearTimeout(btn.property('timout_handler'));
      btn.property('timout_handler', null);
   }

   let is_visible = false;
   switch (action) {
      case 'enable':
         is_visible = true;
         handler.btns_active_flag = true;
         break;
      case 'enterbtn':
         handler.btns_active_flag = true;
         return; // do nothing, just cleanup timeout
      case 'timeout': is_visible = false; break;
      case 'toggle':
         state = !state;
         btn.property('buttons_state', state);
         is_visible = state;
         break;
      case 'disable':
      case 'leavebtn':
         handler.btns_active_flag = false;
         if (!state)
            btn.property('timout_handler', setTimeout(() => toggleButtonsVisibility(handler, 'timeout'), 1200));
         return;
   }

   group.selectAll('svg').each(function() {
      if (this !== btn.node())
         d3_select(this).style('display', is_visible ? '' : 'none');
   });
}

const PadButtonsHandler = {

   alignButtons(btns, width, height) {
      const sz0 = getButtonSize(this, 1.25), nextx = (btns.property('nextx') || 0) + sz0;
      let btns_x, btns_y;

      if (btns.property('vertical')) {
         btns_x = btns.property('leftside') ? 2 : (width - sz0);
         btns_y = height - nextx;
      } else {
         btns_x = btns.property('leftside') ? 2 : (width - nextx);
         btns_y = height - sz0;
      }

      makeTranslate(btns, btns_x, btns_y);
   },

   findPadButton(keyname) {
      const group = this.getLayerSvg('btns_layer', this.this_pad_name);
      let found_func = '';
      if (!group.empty()) {
         group.selectAll('svg').each(function() {
            if (d3_select(this).attr('key') === keyname)
               found_func = d3_select(this).attr('name');
         });
      }
      return found_func;
   },

   removePadButtons() {
      const group = this.getLayerSvg('btns_layer', this.this_pad_name);
      if (!group.empty()) {
         group.selectAll('*').remove();
         group.property('nextx', null);
      }
   },

   showPadButtons() {
      const group = this.getLayerSvg('btns_layer', this.this_pad_name);
      if (group.empty()) return;

      // clean all previous buttons
      group.selectAll('*').remove();
      if (!this._buttons) return;

      const iscan = this.iscan || !this.has_canvas, y = 0;
      let ctrl, x = group.property('leftside') ? getButtonSize(this, 1.25) : 0;

      if (this._fast_drawing) {
         ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.circle, getButtonSize(this), 'enlargePad', false)
                            .attr('name', 'Enlarge').attr('x', 0).attr('y', 0)
                            .on('click', evnt => this.clickPadButton('enlargePad', evnt));
      } else {
         ctrl = ToolbarIcons.createSVG(group, ToolbarIcons.rect, getButtonSize(this), 'Toggle tool buttons', false)
                            .attr('name', 'Toggle').attr('x', 0).attr('y', 0)
                            .property('buttons_state', (settings.ToolBar !== 'popup') || browser.touches)
                            .on('click', evnt => toggleButtonsVisibility(this, 'toggle', evnt));
         ctrl.node()._mouseenter = () => toggleButtonsVisibility(this, 'enable');
         ctrl.node()._mouseleave = () => toggleButtonsVisibility(this, 'disable');

         for (let k = 0; k < this._buttons.length; ++k) {
            const item = this._buttons[k];
            let btn = item.btn;

            if (isStr(btn))
               btn = ToolbarIcons[btn];
            if (!btn)
               btn = ToolbarIcons.circle;

            const svg = ToolbarIcons.createSVG(group, btn, getButtonSize(this),
                        item.tooltip + (iscan ? '' : (` on pad ${this.this_pad_name}`)) + (item.keyname ? ` (keyshortcut ${item.keyname})` : ''), false);

            if (group.property('vertical'))
                svg.attr('x', y).attr('y', x);
            else
               svg.attr('x', x).attr('y', y);

            svg.attr('name', item.funcname)
               .style('display', ctrl.property('buttons_state') ? '' : 'none')
               .attr('key', item.keyname || null)
               .on('click', evnt => this.clickPadButton(item.funcname, evnt));

            svg.node()._mouseenter = () => toggleButtonsVisibility(this, 'enterbtn');
            svg.node()._mouseleave = () => toggleButtonsVisibility(this, 'leavebtn');

            x += getButtonSize(this, 1.25);
         }
      }

      group.property('nextx', x);

      this.alignButtons(group, this.getPadWidth(), this.getPadHeight());

      if (group.property('vertical'))
         ctrl.attr('y', x);
      else if (!group.property('leftside'))
         ctrl.attr('x', x);
   },

   assign(painter) {
      Object.assign(painter, this);
   }

}, // PadButtonsHandler

// identifier used in TWebCanvas painter
webSnapIds = { kNone: 0, kObject: 1, kSVG: 2, kSubPad: 3, kColors: 4, kStyle: 5, kFont: 6 };


/** @summary Fill TWebObjectOptions for painter
  * @private */
function createWebObjectOptions(painter) {
   if (!painter?.snapid)
      return null;

   const obj = { _typename: 'TWebObjectOptions', snapid: painter.snapid.toString(), opt: painter.getDrawOpt(true), fcust: '', fopt: [] };
   if (isFunc(painter.fillWebObjectOptions))
      painter.fillWebObjectOptions(obj);
   return obj;
}


/**
  * @summary Painter for TPad object
  * @private
  */

class TPadPainter extends ObjectPainter {

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} pad - TPad object to draw
     * @param {boolean} [iscan] - if TCanvas object */
   constructor(dom, pad, iscan) {
      super(dom, pad);
      this.pad = pad;
      this.iscan = iscan; // indicate if working with canvas
      this.this_pad_name = '';
      if (!this.iscan && pad?.fName) {
         this.this_pad_name = pad.fName.replace(' ', '_'); // avoid empty symbol in pad name
         const regexp = /^[A-Za-z][A-Za-z0-9_]*$/;
         if (!regexp.test(this.this_pad_name) || ((this.this_pad_name === 'button') && (pad._typename === clTButton)))
            this.this_pad_name = 'jsroot_pad_' + internals.id_counter++;
      }
      this.painters = []; // complete list of all painters in the pad
      this.has_canvas = true;
      this.forEachPainter = this.forEachPainterInPad;
      const d = this.selectDom();
      if (!d.empty() && d.property('_batch_mode'))
         this.batch_mode = true;
   }

   /** @summary Indicates that drawing runs in batch mode
     * @private */
   isBatchMode() {
      if (this.batch_mode !== undefined)
         return this.batch_mode;

      if (isBatchMode())
         return true;

      if (!this.iscan && this.has_canvas)
         return this.getCanvPainter()?.isBatchMode();

      return false;
   }

   /** @summary Indicates that is is Root6 pad painter
    * @private */
   isRoot6() { return true; }

   /** @summary Returns true if pad is editable */
   isEditable() {
      return this.pad?.fEditable ?? true;
   }

   /** @summary Returns SVG element for the pad itself
    * @private */
   svg_this_pad() {
      return this.getPadSvg(this.this_pad_name);
   }

   /** @summary Returns main painter on the pad
     * @desc Typically main painter is TH1/TH2 object which is drawing axes
    * @private */
   getMainPainter() {
      return this.main_painter_ref || null;
   }

   /** @summary Assign main painter on the pad
     * @desc Typically main painter is TH1/TH2 object which is drawing axes
    * @private */
   setMainPainter(painter, force) {
      if (!this.main_painter_ref || force)
         this.main_painter_ref = painter;
   }

   /** @summary cleanup pad and all primitives inside */
   cleanup() {
      if (this._doing_draw)
         console.error('pad drawing is not completed when cleanup is called');

      this.painters.forEach(p => p.cleanup());

      const svg_p = this.svg_this_pad();
      if (!svg_p.empty()) {
         svg_p.property('pad_painter', null);
         if (!this.iscan) svg_p.remove();
      }

      delete this.main_painter_ref;
      delete this.frame_painter_ref;
      delete this.pads_cache;
      delete this.custom_palette;
      delete this._pad_x;
      delete this._pad_y;
      delete this._pad_width;
      delete this._pad_height;
      delete this._doing_draw;
      delete this._interactively_changed;
      delete this._snap_primitives;
      delete this._last_grayscale;
      delete this._custom_colors;
      delete this._custom_palette_indexes;
      delete this._custom_palette_colors;
      delete this.root_colors;

      this.painters = [];
      this.pad = null;
      this.this_pad_name = undefined;
      this.has_canvas = false;

      selectActivePad({ pp: this, active: false });

      super.cleanup();
   }

   /** @summary Returns frame painter inside the pad
     * @private */
   getFramePainter() { return this.frame_painter_ref; }

   /** @summary get pad width */
   getPadWidth() { return this._pad_width || 0; }

   /** @summary get pad height */
   getPadHeight() { return this._pad_height || 0; }

   /** @summary get pad rect */
   getPadRect() {
      return {
         x: this._pad_x || 0,
         y: this._pad_y || 0,
         width: this.getPadWidth(),
         height: this.getPadHeight()
      };
   }

   /** @summary return pad log state x or y are allowed */
   getPadLog(name) {
      const pad = this.getRootPad();
      if (name === 'x')
         return pad?.fLogx;
      if (name === 'y')
         return pad?.fLogv ?? pad?.fLogy;
      return false;
   }

   /** @summary Returns frame coordinates - also when frame is not drawn */
   getFrameRect() {
      const fp = this.getFramePainter();
      if (fp) return fp.getFrameRect();

      const w = this.getPadWidth(),
            h = this.getPadHeight(),
            rect = {};

      if (this.pad) {
         rect.szx = Math.round(Math.max(0, 0.5 - Math.max(this.pad.fLeftMargin, this.pad.fRightMargin))*w);
         rect.szy = Math.round(Math.max(0, 0.5 - Math.max(this.pad.fBottomMargin, this.pad.fTopMargin))*h);
      } else {
         rect.szx = Math.round(0.5*w);
         rect.szy = Math.round(0.5*h);
      }

      rect.width = 2*rect.szx;
      rect.height = 2*rect.szy;
      rect.x = Math.round(w/2 - rect.szx);
      rect.y = Math.round(h/2 - rect.szy);
      rect.hint_delta_x = rect.szx;
      rect.hint_delta_y = rect.szy;
      rect.transform = makeTranslate(rect.x, rect.y) || '';

      return rect;
   }

   /** @summary return RPad object */
   getRootPad(is_root6) {
      return (is_root6 === undefined) || is_root6 ? this.pad : null;
   }

   /** @summary Cleanup primitives from pad - selector lets define which painters to remove
    * @return true if any painter was removed */
   cleanPrimitives(selector) {
      // remove all primitives
      if (selector === true)
         selector = () => true;

      if (!isFunc(selector))
         return false;

      let pad_cleanup = false, is_any = false;

      for (let k = this.painters.length-1; k >= 0; --k) {
         const subp = this.painters[k];
         if (selector(subp)) {
            if (isPadPainter(subp))
               pad_cleanup = true;
            subp.cleanup();
            this.painters.splice(k, 1);
            is_any = true;
         }
      }

      if (pad_cleanup) {
         const cp = this.getCanvPainter();
         if (cp) delete cp.pads_cache;
      }

      return is_any;
   }

   /** @summary Removes and cleanup specified primitive
     * @desc also secondary primitives will be removed
     * @return new index to continue loop or -111 if main painter removed
     * @private */
   removePrimitive(arg, clean_only_secondary) {
      let indx = -1, prim = null;
      if (Number.isInteger(arg)) {
         indx = arg; prim = this.painters[indx];
      } else {
         indx = this.painters.indexOf(arg); prim = arg;
      }
      if (indx < 0)
         return indx;

      const arr = [], get_main = clean_only_secondary ? this.getMainPainter() : null;
      let resindx = indx - 1; // object removed itself
      arr.push(prim);
      this.painters.splice(indx, 1);

      // loop to extract all dependent painters
      let len0 = 0;
      while (len0 < arr.length) {
         for (let k = this.painters.length-1; k >= 0; --k) {
            if (this.painters[k].isSecondary(arr[len0])) {
               arr.push(this.painters[k]);
               this.painters.splice(k, 1);
               if (k < indx) resindx--;
            }
         }
         len0++;
      }

      arr.forEach(painter => {
         if ((painter !== prim) || !clean_only_secondary)
            painter.cleanup();
         if (this.main_painter_ref === painter) {
            delete this.main_painter_ref;
            resindx = -111;
         }
      });

      // when main painter disappears because of special cleanup - also reset zooming
      if (clean_only_secondary && get_main && !this.getMainPainter())
         this.getFramePainter()?.resetZoom();

      return resindx;
   }

  /** @summary returns custom palette associated with pad or top canvas
    * @private */
   getCustomPalette() {
      return this.custom_palette || this.getCanvPainter()?.custom_palette;
   }

   /** @summary Returns number of painters
     * @private */
   getNumPainters() { return this.painters.length; }

   /** @summary Provides automatic color
    * @desc Uses ROOT colors palette if possible
    * @private */
   getAutoColor(numprimitives) {
      if (!numprimitives)
         numprimitives = (this._num_primitives || 5) - (this._num_specials || 0);
      if (numprimitives < 2) numprimitives = 2;

      let indx = this._auto_color ?? 0;
      this._auto_color = (indx + 1) % numprimitives;
      if (indx >= numprimitives) indx = numprimitives - 1;

      const indexes = this._custom_palette_indexes || this.getCanvPainter()?._custom_palette_indexes;

      if (indexes?.length) {
         const p = Math.round(indx * (indexes.length - 3) / (numprimitives - 1));
         return indexes[p];
      }

      if (!this._auto_palette)
         this._auto_palette = getColorPalette(settings.Palette, this.isGrayscale());
      const palindx = Math.round(indx * (this._auto_palette.getLength()-3) / (numprimitives-1)),
            colvalue = this._auto_palette.getColor(palindx);

      return this.addColor(colvalue);
   }

   /** @summary Call function for each painter in pad
     * @param {function} userfunc - function to call
     * @param {string} kind - 'all' for all objects (default), 'pads' only pads and sub-pads, 'objects' only for object in current pad
     * @private */
   forEachPainterInPad(userfunc, kind) {
      if (!kind) kind = 'all';
      if (kind !== 'objects') userfunc(this);
      for (let k = 0; k < this.painters.length; ++k) {
         const sub = this.painters[k];
         if (isFunc(sub.forEachPainterInPad)) {
            if (kind !== 'objects') sub.forEachPainterInPad(userfunc, kind);
         } else if (kind !== 'pads')
            userfunc(sub);
      }
   }

   /** @summary register for pad events receiver
     * @desc in pad painter, while pad may be drawn without canvas */
   registerForPadEvents(receiver) {
      this.pad_events_receiver = receiver;
   }

   /** @summary Generate pad events, normally handled by GED
     * @desc in pad painter, while pad may be drawn without canvas
     * @private */
   producePadEvent(what, padpainter, painter, position) {
      if ((what === 'select') && isFunc(this.selectActivePad))
         this.selectActivePad(padpainter, painter, position);

      if (isFunc(this.pad_events_receiver))
         this.pad_events_receiver({ what, padpainter, painter, position });
   }

   /** @summary method redirect call to pad events receiver */
   selectObjectPainter(painter, pos) {
      const istoppad = this.iscan || !this.has_canvas,
            canp = istoppad ? this : this.getCanvPainter();

      if (painter === undefined)
         painter = this;

      if (pos && !istoppad)
         pos = getAbsPosInCanvas(this.svg_this_pad(), pos);

      selectActivePad({ pp: this, active: true });

      canp?.producePadEvent('select', this, painter, pos);
   }

   /** @summary Draw pad active border
    * @private */
   drawActiveBorder(svg_rect, is_active) {
      if (is_active !== undefined) {
         if (this.is_active_pad === is_active) return;
         this.is_active_pad = is_active;
      }

      if (this.is_active_pad === undefined) return;

      if (!svg_rect)
         svg_rect = this.iscan ? this.getCanvSvg().selectChild('.canvas_fillrect') : this.svg_this_pad().selectChild('.root_pad_border');

      const cp = this.getCanvPainter();

      let lineatt = this.is_active_pad && cp?.highlight_gpad ? new TAttLineHandler({ style: 1, width: 1, color: 'red' }) : this.lineatt;

      if (!lineatt) lineatt = new TAttLineHandler({ color: 'none' });

      svg_rect.call(lineatt.func);
   }

   /** @summary Set fast drawing property depending on the size
     * @private */
   setFastDrawing(w, h) {
      const was_fast = this._fast_drawing;
      this._fast_drawing = settings.SmallPad && ((w < settings.SmallPad.width) || (h < settings.SmallPad.height));
      if (was_fast !== this._fast_drawing)
         this.showPadButtons();
   }

   /** @summary Returns true if canvas configured with grayscale
     * @private */
   isGrayscale() {
      if (!this.iscan) return false;
      return this.pad?.TestBit(kIsGrayscale) ?? false;
   }

   /** @summary Set grayscale mode for the canvas
     * @private */
   setGrayscale(flag) {
      if (!this.iscan) return;

      let changed = false;

      if (flag === undefined) {
         flag = this.pad?.TestBit(kIsGrayscale) ?? false;
         changed = (this._last_grayscale !== undefined) && (this._last_grayscale !== flag);
      } else if (flag !== this.pad?.TestBit(kIsGrayscale)) {
         this.pad?.InvertBit(kIsGrayscale);
         changed = true;
      }

      if (changed)
         this.forEachPainter(p => { delete p._color_palette; });

      this.root_colors = flag ? getGrayColors(this._custom_colors) : this._custom_colors;

      this._last_grayscale = flag;

      this.custom_palette = this._custom_palette_colors ? new ColorPalette(this._custom_palette_colors, flag) : null;
   }

   /** @summary Create SVG element for canvas */
   createCanvasSvg(check_resize, new_size) {
      const is_batch = this.isBatchMode(), lmt = 5;
      let factor = null, svg = null, rect = null, btns, info, frect;

      if (check_resize > 0) {
         if (this._fixed_size)
            return check_resize > 1; // flag used to force re-drawing of all sub-pads

         svg = this.getCanvSvg();
         if (svg.empty())
            return false;

         factor = svg.property('height_factor');

         rect = this.testMainResize(check_resize, null, factor);

         if (!rect.changed && (check_resize === 1))
            return false;

         if (!is_batch)
            btns = this.getLayerSvg('btns_layer', this.this_pad_name);

         info = this.getLayerSvg('info_layer', this.this_pad_name);
         frect = svg.selectChild('.canvas_fillrect');
      } else {
         const render_to = this.selectDom();

         if (render_to.style('position') === 'static')
            render_to.style('position', 'relative');

         svg = render_to.append('svg')
             .attr('class', 'jsroot root_canvas')
             .property('pad_painter', this) // this is custom property
             .property('redraw_by_resize', false); // could be enabled to force redraw by each resize

         this.setTopPainter(); // assign canvas as top painter of that element

         if (is_batch)
            svg.attr('xmlns', nsSVG);
         else if (!this.online_canvas)
            svg.append('svg:title').text('ROOT canvas');

         if (!is_batch || (this.pad.fFillStyle > 0))
            frect = svg.append('svg:path').attr('class', 'canvas_fillrect');

         if (!is_batch) {
            frect.style('pointer-events', 'visibleFill')
                 .on('dblclick', evnt => this.enlargePad(evnt, true))
                 .on('click', () => this.selectObjectPainter())
                 .on('mouseenter', () => this.showObjectStatus())
                 .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null);
         }

         svg.append('svg:g').attr('class', 'primitives_layer');
         info = svg.append('svg:g').attr('class', 'info_layer');
         if (!is_batch) {
            btns = svg.append('svg:g')
                      .attr('class', 'btns_layer')
                      .property('leftside', settings.ToolBarSide === 'left')
                      .property('vertical', settings.ToolBarVert);
         }

         factor = 0.66;
         if (this.pad?.fCw && this.pad?.fCh && (this.pad?.fCw > 0)) {
            factor = this.pad.fCh / this.pad.fCw;
            if ((factor < 0.1) || (factor > 10)) factor = 0.66;
         }

         if (this._fixed_size) {
            render_to.style('overflow', 'auto');
            rect = { width: this.pad.fCw, height: this.pad.fCh };
            if (!rect.width || !rect.height)
               rect = getElementRect(render_to);
         } else
            rect = this.testMainResize(2, new_size, factor);
      }

      this.setGrayscale();

      this.createAttFill({ attr: this.pad });

      if ((rect.width <= lmt) || (rect.height <= lmt)) {
         svg.style('display', 'none');
         console.warn(`Hide canvas while geometry too small w=${rect.width} h=${rect.height}`);
         if (this._pad_width && this._pad_height) {
            // use last valid dimensions
            rect.width = this._pad_width;
            rect.height = this._pad_height;
         } else {
            // just to complete drawing.
            rect.width = 800;
            rect.height = 600;
         }
      } else
         svg.style('display', null);

      svg.attr('x', 0).attr('y', 0).style('position', 'absolute');

      if (this._fixed_size)
         svg.attr('width', rect.width).attr('height', rect.height);
      else
         svg.style('width', '100%').style('height', '100%').style('left', 0).style('top', 0).style('bottom', 0).style('right', 0);

      svg.style('filter', settings.DarkMode || this.pad?.$dark ? 'invert(100%)' : null);

      svg.attr('viewBox', `0 0 ${rect.width} ${rect.height}`)
         .attr('preserveAspectRatio', 'none')  // we do not preserve relative ratio
         .property('height_factor', factor)
         .property('draw_x', 0)
         .property('draw_y', 0)
         .property('draw_width', rect.width)
         .property('draw_height', rect.height);

      this._pad_x = 0;
      this._pad_y = 0;
      this._pad_width = rect.width;
      this._pad_height = rect.height;

      if (frect) {
         frect.attr('d', `M0,0H${rect.width}V${rect.height}H0Z`)
              .call(this.fillatt.func);
         this.drawActiveBorder(frect);
      }

      this.setFastDrawing(rect.width * (1 - this.pad.fLeftMargin - this.pad.fRightMargin), rect.height * (1 - this.pad.fBottomMargin - this.pad.fTopMargin));

      if (this.alignButtons && btns)
         this.alignButtons(btns, rect.width, rect.height);

      let dt = info.selectChild('.canvas_date');
      if (!gStyle.fOptDate)
         dt.remove();
       else {
         if (dt.empty())
             dt = info.append('text').attr('class', 'canvas_date');
         const posy = Math.round(rect.height * (1 - gStyle.fDateY)),
               date = new Date();
         let posx = Math.round(rect.width * gStyle.fDateX);
         if (!is_batch && (posx < 25))
            posx = 25;
         if (gStyle.fOptDate > 3)
            date.setTime(gStyle.fOptDate*1000);

         makeTranslate(dt, posx, posy)
            .style('text-anchor', 'start')
            .text(convertDate(date));
      }

      const iname = this.getItemName();
      if (iname)
         this.drawItemNameOnCanvas(iname);
      else if (!gStyle.fOptFile)
         info.selectChild('.canvas_item').remove();

      return true;
   }

   /** @summary Draw item name on canvas if gStyle.fOptFile is configured
     * @private */
   drawItemNameOnCanvas(item_name) {
      const info = this.getLayerSvg('info_layer', this.this_pad_name);
      let df = info.selectChild('.canvas_item');
      const fitem = getHPainter().findRootFileForItem(item_name),
            fname = (gStyle.fOptFile === 3) ? item_name : ((gStyle.fOptFile === 2) ? fitem?._fullurl : fitem?._name);

      if (!gStyle.fOptFile || !fname)
         df.remove();
       else {
         if (df.empty())
            df = info.append('text').attr('class', 'canvas_item');
         const rect = this.getPadRect();
         makeTranslate(df, Math.round(rect.width * (1 - gStyle.fDateX)), Math.round(rect.height * (1 - gStyle.fDateY)))
            .style('text-anchor', 'end')
            .text(fname);
      }
      if (((gStyle.fOptDate === 2) || (gStyle.fOptDate === 3)) && fitem?._file) {
         info.selectChild('.canvas_date')
             .text(convertDate(getTDatime(gStyle.fOptDate === 2 ? fitem._file.fDatimeC : fitem._file.fDatimeM)));
      }
   }

   /** @summary Return true if this pad enlarged */
   isPadEnlarged() {
      if (this.iscan || !this.has_canvas)
         return this.enlargeMain('state') === 'on';
      return this.getCanvSvg().property('pad_enlarged') === this.pad;
   }

   /** @summary Enlarge pad draw element when possible */
   enlargePad(evnt, is_dblclick, is_escape) {
      evnt?.preventDefault();
      evnt?.stopPropagation();

      // ignore double click on canvas itself for enlarge
      if (is_dblclick && this._websocket && (this.enlargeMain('state') === 'off'))
         return;

      const svg_can = this.getCanvSvg(),
            pad_enlarged = svg_can.property('pad_enlarged');

      if (this.iscan || !this.has_canvas || (!pad_enlarged && !this.hasObjectsToDraw() && !this.painters)) {
         if (this._fixed_size) return; // canvas cannot be enlarged in such mode
         if (!this.enlargeMain(is_escape ? false : 'toggle')) return;
         if (this.enlargeMain('state') === 'off')
            svg_can.property('pad_enlarged', null);
         else
            selectActivePad({ pp: this, active: true });
      } else if (!pad_enlarged && !is_escape) {
         this.enlargeMain(true, true);
         svg_can.property('pad_enlarged', this.pad);
         selectActivePad({ pp: this, active: true });
      } else if (pad_enlarged === this.pad) {
         this.enlargeMain(false);
         svg_can.property('pad_enlarged', null);
      } else if (!is_escape && is_dblclick)
         console.error('missmatch with pad double click events');

      return this.checkResize(true);
   }

   /** @summary Create main SVG element for pad
     * @return true when pad is displayed and all its items should be redrawn */
   createPadSvg(only_resize) {
      if (!this.has_canvas) {
         this.createCanvasSvg(only_resize ? 2 : 0);
         return true;
      }

      const svg_can = this.getCanvSvg(),
            width = svg_can.property('draw_width'),
            height = svg_can.property('draw_height'),
            pad_enlarged = svg_can.property('pad_enlarged'),
            pad_visible = !this.pad_draw_disabled && (!pad_enlarged || (pad_enlarged === this.pad)),
            is_batch = this.isBatchMode();
      let w = Math.round(this.pad.fAbsWNDC * width),
          h = Math.round(this.pad.fAbsHNDC * height),
          x = Math.round(this.pad.fAbsXlowNDC * width),
          y = Math.round(height * (1 - this.pad.fAbsYlowNDC)) - h,
          svg_pad, svg_border, btns;

      if (pad_enlarged === this.pad) { w = width; h = height; x = y = 0; }

      if (only_resize) {
         svg_pad = this.svg_this_pad();
         svg_border = svg_pad.selectChild('.root_pad_border');
         if (!is_batch)
            btns = this.getLayerSvg('btns_layer', this.this_pad_name);
         this.addPadInteractive(true);
      } else {
         svg_pad = svg_can.selectChild('.primitives_layer')
             .append('svg:svg') // svg used to blend all drawings outside
             .classed('__root_pad_' + this.this_pad_name, true)
             .attr('pad', this.this_pad_name) // set extra attribute  to mark pad name
             .property('pad_painter', this); // this is custom property

         if (!is_batch)
            svg_pad.append('svg:title').text('subpad ' + this.this_pad_name);

         // need to check attributes directly while attributes objects will be created later
         if (!is_batch || (this.pad.fFillStyle > 0) || ((this.pad.fLineStyle > 0) && (this.pad.fLineColor > 0)))
            svg_border = svg_pad.append('svg:path').attr('class', 'root_pad_border');

         if (!is_batch) {
            svg_border.style('pointer-events', 'visibleFill') // get events also for not visible rect
                      .on('dblclick', evnt => this.enlargePad(evnt, true))
                      .on('click', () => this.selectObjectPainter())
                      .on('mouseenter', () => this.showObjectStatus())
                      .on('contextmenu', settings.ContextMenu ? evnt => this.padContextMenu(evnt) : null);
         }

         svg_pad.append('svg:g').attr('class', 'primitives_layer');
         if (!is_batch) {
            btns = svg_pad.append('svg:g')
                          .attr('class', 'btns_layer')
                          .property('leftside', settings.ToolBarSide !== 'left')
                          .property('vertical', settings.ToolBarVert);
         }
      }

      this.createAttFill({ attr: this.pad });
      this.createAttLine({ attr: this.pad, color0: !this.pad.fBorderMode ? 'none' : '' });

      svg_pad.style('display', pad_visible ? null : 'none')
             .attr('viewBox', `0 0 ${w} ${h}`) // due to svg
             .attr('preserveAspectRatio', 'none')   // due to svg, we do not preserve relative ratio
             .attr('x', x)        // due to svg
             .attr('y', y)        // due to svg
             .attr('width', w)    // due to svg
             .attr('height', h)   // due to svg
             .property('draw_x', x) // this is to make similar with canvas
             .property('draw_y', y)
             .property('draw_width', w)
             .property('draw_height', h);

      this._pad_x = x;
      this._pad_y = y;
      this._pad_width = w;
      this._pad_height = h;

      if (svg_border) {
         svg_border.attr('d', `M0,0H${w}V${h}H0Z`)
                   .call(this.fillatt.func)
                   .call(this.lineatt.func);
         this.drawActiveBorder(svg_border);

         let svg_border1 = svg_pad.selectChild('.root_pad_border1'),
             svg_border2 = svg_pad.selectChild('.root_pad_border2');

         if (this.pad.fBorderMode && this.pad.fBorderSize) {
            const pw = this.pad.fBorderSize, ph = this.pad.fBorderSize,
                side1 = `M0,0h${w}l${-pw},${ph}h${2*pw-w}v${h-2*ph}l${-pw},${ph}z`,
                side2 = `M${w},${h}v${-h}l${-pw},${ph}v${h-2*ph}h${2*pw-w}l${-pw},${ph}z`;

            if (svg_border2.empty())
               svg_border2 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border2');
            if (svg_border1.empty())
               svg_border1 = svg_pad.insert('svg:path', '.primitives_layer').attr('class', 'root_pad_border1');

            svg_border1.attr('d', this.pad.fBorderMode > 0 ? side1 : side2)
                       .call(this.fillatt.func)
                       .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb());
            svg_border2.attr('d', this.pad.fBorderMode > 0 ? side2 : side1)
                       .call(this.fillatt.func)
                       .style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatRgb());
         } else {
            svg_border1.remove();
            svg_border2.remove();
         }
      }

      this.setFastDrawing(w * (1 - this.pad.fLeftMargin-this.pad.fRightMargin), h * (1 - this.pad.fBottomMargin - this.pad.fTopMargin));

      // special case of 3D canvas overlay
      if (svg_pad.property('can3d') === constants.Embed3D.Overlay) {
         this.selectDom().select('.draw3d_' + this.this_pad_name)
              .style('display', pad_visible ? '' : 'none');
      }

      if (this.alignButtons && btns)
         this.alignButtons(btns, w, h);

      return pad_visible;
   }

   /** @summary Add pad interactive features like dragging and resize
     * @private */
   addPadInteractive(cleanup = false) {
      if (isFunc(this.$userInteractive)) {
         this.$userInteractive();
         delete this.$userInteractive;
      }

      if (this.isBatchMode() || this.iscan)
         return;

      const svg_can = this.getCanvSvg(),
            width = svg_can.property('draw_width'),
            height = svg_can.property('draw_height');

      addDragHandler(this, {
         cleanup, // do cleanup to let assign new handlers later on
         x: this._pad_x, y: this._pad_y, width: this._pad_width, height: this._pad_height, no_transform: true,
         only_resize: true, // !cleanup && (this._disable_dragging || this.getFramePainter()?.mode3d),
         is_disabled: kind => svg_can.property('pad_enlarged') || this.btns_active_flag ||
                             (kind === 'move' && (this._disable_dragging || this.getFramePainter()?.mode3d)),
         getDrawG: () => this.svg_this_pad(),
         pad_rect: { width, height },
         minwidth: 20, minheight: 20,
         move_resize: (_x, _y, _w, _h) => {
            const x0 = this.pad.fAbsXlowNDC,
                y0 = this.pad.fAbsYlowNDC,
                scale_w = _w / width / this.pad.fAbsWNDC,
                scale_h = _h / height / this.pad.fAbsHNDC,
                shift_x = _x / width - x0,
                shift_y = 1 - (_y + _h) / height - y0;
            this.forEachPainterInPad(p => {
               p.pad.fAbsXlowNDC += (p.pad.fAbsXlowNDC - x0) * (scale_w - 1) + shift_x;
               p.pad.fAbsYlowNDC += (p.pad.fAbsYlowNDC - y0) * (scale_h - 1) + shift_y;
               p.pad.fAbsWNDC *= scale_w;
               p.pad.fAbsHNDC *= scale_h;
            }, 'pads');
         },
         redraw: () => this.interactiveRedraw('pad', 'padpos')
      });
   }

   /** @summary Disable pad drawing
     * @desc Complete SVG element will be hidden */
   disablePadDrawing() {
      if (!this.pad_draw_disabled && this.has_canvas && !this.iscan) {
         this.pad_draw_disabled = true;
         this.createPadSvg(true);
      }
   }

   /** @summary Check if it is special object, which should be handled separately
     * @desc It can be TStyle or list of colors or palette object
     * @return {boolean} true if any */
   checkSpecial(obj) {
      if (!obj) return false;

      if (obj._typename === clTStyle) {
         Object.assign(gStyle, obj);
         return true;
      }

      if ((obj._typename === clTObjArray) && (obj.name === 'ListOfColors')) {
         if (this.options?.CreatePalette) {
            let arr = [];
            for (let n = obj.arr.length - this.options.CreatePalette; n < obj.arr.length; ++n) {
               const col = getRGBfromTColor(obj.arr[n]);
               if (!col) { console.log('Fail to create color for palette'); arr = null; break; }
               arr.push(col);
            }
            if (arr) this.custom_palette = new ColorPalette(arr);
         }

         if (!this.options || this.options.GlobalColors) // set global list of colors
            adoptRootColors(obj);

         // copy existing colors and extend with new values
         this._custom_colors = this.options?.LocalColors ? extendRootColors(null, obj) : null;
         return true;
      }

      if ((obj._typename === clTObjArray) && (obj.name === 'CurrentColorPalette')) {
         const arr = [], indx = [];
         let missing = false;
         for (let n = 0; n < obj.arr.length; ++n) {
            const col = obj.arr[n];
            if (col?._typename === clTColor) {
               indx[n] = col.fNumber;
               arr[n] = getRGBfromTColor(col);
            } else {
               console.log(`Missing color with index ${n}`);
               missing = true;
            }
         }

         const apply = (!this.options || (!missing && !this.options.IgnorePalette));
         this._custom_palette_indexes = apply ? indx : null;
         this._custom_palette_colors = apply ? arr : null;

         return true;
      }

      return false;
   }

   /** @summary Check if special objects appears in primitives
     * @desc it could be list of colors or palette */
   checkSpecialsInPrimitives(can, count_specials) {
      const lst = can?.fPrimitives;
      if (count_specials)
         this._num_specials = 0;
      if (!lst) return;
      for (let i = 0; i < lst.arr?.length; ++i) {
         if (this.checkSpecial(lst.arr[i])) {
            lst.arr[i].$special = true; // mark object as special one, do not use in drawing
            if (count_specials)
               this._num_specials++;
         }
      }
   }

   /** @summary try to find object by name in list of pad primitives
     * @desc used to find title drawing
     * @private */
   findInPrimitives(objname, objtype) {
      const match = obj => obj && (obj?.fName === objname) && (objtype ? (obj?._typename === objtype) : true),
            snap = this._snap_primitives?.find(snap => match((snap.fKind === webSnapIds.kObject) ? snap.fSnapshot : null));
      if (snap) return snap.fSnapshot;

      return this.pad?.fPrimitives?.arr.find(match);
   }

   /** @summary Try to find painter for specified object
     * @desc can be used to find painter for some special objects, registered as
     * histogram functions
     * @param {object} selobj - object to which painter should be search, set null to ignore parameter
     * @param {string} [selname] - object name, set to null to ignore
     * @param {string} [seltype] - object type, set to null to ignore
     * @return {object} - painter for specified object (if any)
     * @private */
   findPainterFor(selobj, selname, seltype) {
      return this.painters.find(p => {
         const pobj = p.getObject();
         if (!pobj) return false;

         if (selobj && (pobj === selobj)) return true;
         if (!selname && !seltype) return false;
         if (selname && (pobj.fName !== selname)) return false;
         if (seltype && (pobj._typename !== seltype)) return false;
         return true;
      });
   }

   /** @summary Return true if any objects beside sub-pads exists in the pad */
   hasObjectsToDraw() {
      return this.pad?.fPrimitives?.arr?.find(obj => obj._typename !== clTPad);
   }

   /** @summary sync drawing/redrawing/resize of the pad
     * @param {string} kind - kind of draw operation, if true - always queued
     * @return {Promise} when pad is ready for draw operation or false if operation already queued
     * @private */
   syncDraw(kind) {
      const entry = { kind: kind || 'redraw' };
      if (this._doing_draw === undefined) {
         this._doing_draw = [entry];
         return Promise.resolve(true);
      }
      // if queued operation registered, ignore next calls, indx === 0 is running operation
      if ((entry.kind !== true) && (this._doing_draw.findIndex((e, i) => (i > 0) && (e.kind === entry.kind)) > 0))
         return false;
      this._doing_draw.push(entry);
      return new Promise(resolveFunc => {
         entry.func = resolveFunc;
      });
   }

   /** @summary indicates if painter performing objects draw
     * @private */
   doingDraw() {
      return this._doing_draw !== undefined;
   }

   /** @summary confirms that drawing is completed, may trigger next drawing immediately
     * @private */
   confirmDraw() {
      if (this._doing_draw === undefined)
         return console.warn('failure, should not happen');
      this._doing_draw.shift();
      if (this._doing_draw.length === 0)
         delete this._doing_draw;
       else {
         const entry = this._doing_draw[0];
         if (entry.func) { entry.func(); delete entry.func; }
      }
   }

   /** @summary Draw single primitive */
   async drawObject(/* dom, obj, opt */) {
      console.log('Not possible to draw object without loading of draw.mjs');
      return null;
   }

   /** @summary Draw pad primitives
     * @return {Promise} when drawing completed
     * @private */
   async drawPrimitives(indx) {
      if (indx === undefined) {
         if (this.iscan)
            this._start_tm = new Date().getTime();

         // set number of primitives
         this._num_primitives = this.pad?.fPrimitives?.arr?.length || 0;

         // sync to prevent immediate pad redraw during normal drawing sequence
         return this.syncDraw(true).then(() => this.drawPrimitives(0));
      }

      if (!this.pad || (indx >= this._num_primitives)) {
         if (this._start_tm) {
            const spenttm = new Date().getTime() - this._start_tm;
            if (spenttm > 1000) console.log(`Canvas ${this.pad?.fName || '---'} drawing took ${(spenttm*1e-3).toFixed(2)}s`);
            delete this._start_tm;
         }

         this.confirmDraw();
         return;
      }

      const obj = this.pad.fPrimitives.arr[indx];

      if (!obj || obj.$special || ((indx > 0) && (obj._typename === clTFrame) && this.getFramePainter()))
         return this.drawPrimitives(indx+1);

      // use of Promise should avoid large call-stack depth when many primitives are drawn
      return this.drawObject(this, obj, this.pad.fPrimitives.opt[indx]).then(op => {
         if (isObject(op))
            op._primitive = true; // mark painter as belonging to primitives

         return this.drawPrimitives(indx+1);
      });
   }

   /** @summary Divide pad on sub-pads
     * @return {Promise} when finished
     * @private */
   async divide(nx, ny, use_existing) {
      if (nx && !ny && use_existing) {
         for (let k = 0; k < nx; ++k) {
            if (!this.getSubPadPainter(k+1)) {
               use_existing = false;
               break;
            }
         }
         if (use_existing)
            return this;
      }

      this.cleanPrimitives(isPadPainter);
      if (!this.pad.fPrimitives)
         this.pad.fPrimitives = create(clTList);
      this.pad.fPrimitives.Clear();

      if ((!nx && !ny) || !this.pad.Divide(nx, ny))
         return this;

      const drawNext = indx => {
         if (indx >= this.pad.fPrimitives.arr.length)
            return this;
         return this.drawObject(this, this.pad.fPrimitives.arr[indx]).then(() => drawNext(indx + 1));
      };

      return drawNext(0);
   }

   /** @summary Return sub-pads painter, only direct childs are checked
     * @private */
   getSubPadPainter(n) {
      for (let k = 0; k < this.painters.length; ++k) {
         const sub = this.painters[k];
         if (isPadPainter(sub) && (sub.pad.fNumber === n))
            return sub;
      }
      return null;
   }

   /** @summary Process tooltip event in the pad
     * @private */
   processPadTooltipEvent(pnt) {
      const painters = [], hints = [];

      // first count - how many processors are there
      this.painters?.forEach(obj => {
         if (isFunc(obj.processTooltipEvent))
            painters.push(obj);
      });

      if (pnt) pnt.nproc = painters.length;

      painters.forEach(obj => {
         const hint = obj.processTooltipEvent(pnt) || { user_info: null };
         hints.push(hint);
         if (pnt?.painters) hint.painter = obj;
      });

      return hints;
   }

   /** @summary Changes canvas dark mode
     * @private */
   changeDarkMode(mode) {
      this.getCanvSvg().style('filter', (mode ?? settings.DarkMode) ? 'invert(100%)' : null);
   }

   /** @summary Fill pad context menu
     * @private */
   fillContextMenu(menu) {
      if (this.pad)
         menu.header(`${this.pad._typename}::${this.pad.fName}`, `${urlClassPrefix}${this.pad._typename}.html`);
      else
         menu.header('Canvas', `${urlClassPrefix}${clTCanvas}.html`);

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

      if (!this._websocket) {
         const set_pad_field = arg => {
            this.pad[arg.slice(1)] = Number.parseInt(arg[0]);
            this.interactiveRedraw('pad', arg.slice(1));
         }, do_divide = arg => {
            if (!arg || !isStr(arg))
               return;
            const arr = arg.split('x');
            this.cleanPrimitives(true);
            if (arr.length === 1)
               this.divide(Number.parseInt(arr[0]));
            if (arr.length === 2)
               this.divide(Number.parseInt(arr[0]), Number.parseInt(arr[1]));
         };

         menu.addchk(this.pad?.fGridx, 'Grid x', (this.pad?.fGridx ? '0' : '1') + 'fGridx', set_pad_field);
         menu.addchk(this.pad?.fGridy, 'Grid y', (this.pad?.fGridy ? '0' : '1') + 'fGridy', set_pad_field);
         menu.sub('Ticks x');
         menu.addchk(this.pad?.fTickx === 0, 'normal', '0fTickx', set_pad_field);
         menu.addchk(this.pad?.fTickx === 1, 'ticks on both sides', '1fTickx', set_pad_field);
         menu.addchk(this.pad?.fTickx === 2, 'labels on both sides', '2fTickx', set_pad_field);
         menu.endsub();
         menu.sub('Ticks y');
         menu.addchk(this.pad?.fTicky === 0, 'normal', '0fTicky', set_pad_field);
         menu.addchk(this.pad?.fTicky === 1, 'ticks on both sides', '1fTicky', set_pad_field);
         menu.addchk(this.pad?.fTicky === 2, 'labels on both sides', '2fTicky', set_pad_field);
         menu.endsub();
         menu.addchk(this.pad?.fEditable, 'Editable', flag => { this.pad.fEditable = flag; this.interactiveRedraw('pad'); });
         if (this.iscan)
            menu.addchk(this.pad?.TestBit(kIsGrayscale), 'Gray scale', flag => { this.setGrayscale(flag); this.interactiveRedraw('pad'); });

         if (isFunc(this.drawObject))
            menu.add('Build legend', () => this.buildLegend());

         menu.sub('Divide', () => menu.input('Input divide arg', '2x2').then(do_divide), 'Divide on sub-pads');
         ['1x2', '2x1', '2x2', '2x3', '3x2', '3x3', '4x4', '0'].forEach(item => menu.add(item, item, do_divide));
         menu.endsub();

         menu.addAttributesMenu(this);
         menu.add('Save to gStyle', () => {
            if (!this.pad) return;
            this.fillatt?.saveToStyle(this.iscan ? 'fCanvasColor' : 'fPadColor');
            gStyle.fPadGridX = this.pad.fGridx;
            gStyle.fPadGridY = this.pad.fGridy;
            gStyle.fPadTickX = this.pad.fTickx;
            gStyle.fPadTickY = this.pad.fTicky;
            gStyle.fOptLogx = this.pad.fLogx;
            gStyle.fOptLogy = this.pad.fLogy;
            gStyle.fOptLogz = this.pad.fLogz;
         }, 'Store pad fill attributes, grid, tick and log scale settings to gStyle');

         if (this.iscan) {
            menu.addSettingsMenu(false, false, arg => {
               if (arg === 'dark') this.changeDarkMode();
            });
         }
      }

      menu.separator();

      if (isFunc(this.hasMenuBar) && isFunc(this.actiavteMenuBar))
         menu.addchk(this.hasMenuBar(), 'Menu bar', flag => this.actiavteMenuBar(flag));

      if (isFunc(this.hasEventStatus) && isFunc(this.activateStatusBar) && isFunc(this.canStatusBar)) {
         if (this.canStatusBar())
            menu.addchk(this.hasEventStatus(), 'Event status', () => this.activateStatusBar('toggle'));
      }

      if (this.enlargeMain() || (this.has_canvas && this.hasObjectsToDraw()))
         menu.addchk(this.isPadEnlarged(), 'Enlarge ' + (this.iscan ? 'canvas' : 'pad'), () => this.enlargePad());

      const fname = this.this_pad_name || (this.iscan ? 'canvas' : 'pad');
      menu.sub('Save as');
      const fmts = ['svg', 'png', 'jpeg', 'webp'];
      if (internals.makePDF) fmts.push('pdf');
      fmts.forEach(fmt => menu.add(`${fname}.${fmt}`, () => this.saveAs(fmt, this.iscan, `${fname}.${fmt}`)));
      if (this.iscan) {
         menu.separator();
         menu.add(`${fname}.json`, () => this.saveAs('json', true, `${fname}.json`), 'Produce JSON with line spacing');
         menu.add(`${fname}0.json`, () => this.saveAs('json', false, `${fname}0.json`), 'Produce JSON without line spacing');
      }
      menu.endsub();

      return true;
   }

   /** @summary Show pad context menu
     * @private */
   async padContextMenu(evnt) {
      if (evnt.stopPropagation) {
         // this is normal event processing and not emulated jsroot event
         evnt.stopPropagation(); // disable main context menu
         evnt.preventDefault();  // disable browser context menu
         this.getFramePainter()?.setLastEventPos();
      }

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

   /** @summary Redraw TLegend object
    * @desc Used when object attributes are changed to ensure that legend is up to date
    * @private */
   async redrawLegend() {
      return this.findPainterFor(null, '', clTLegend)?.redraw();
   }

   /** @summary Redraw pad means redraw ourself
     * @return {Promise} when redrawing ready */
   async redrawPad(reason) {
      const sync_promise = this.syncDraw(reason);
      if (sync_promise === false) {
         console.log(`Prevent redrawing of ${this.pad.fName}`);
         return false;
      }

      let showsubitems = true;
      const redrawNext = indx => {
         while (indx < this.painters.length) {
            const sub = this.painters[indx++];
            let res = 0;
            if (showsubitems || sub.this_pad_name)
               res = sub.redraw(reason);

            if (isPromise(res))
               return res.then(() => redrawNext(indx));
         }
         return true;
      };

      return sync_promise.then(() => {
         if (this.iscan)
            this.createCanvasSvg(2);
         else
            showsubitems = this.createPadSvg(true);
         return redrawNext(0);
      }).then(() => {
         this.addPadInteractive();
         this.confirmDraw();
         if (getActivePad() === this)
            this.getCanvPainter()?.producePadEvent('padredraw', this);
         return true;
      });
   }

   /** @summary redraw pad */
   redraw(reason) {
      // intentionally do not return Promise to let re-draw sub-pads in parallel
      this.redrawPad(reason);
   }

   /** @summary Checks if pad should be redrawn by resize
     * @private */
   needRedrawByResize() {
      const elem = this.svg_this_pad();
      if (!elem.empty() && elem.property('can3d') === constants.Embed3D.Overlay) return true;

      return this.painters.findIndex(objp => {
         return isFunc(objp.needRedrawByResize) ? objp.needRedrawByResize() : false;
      }) >= 0;
   }

   /** @summary Check resize of canvas
     * @return {Promise} with result or false */
   checkCanvasResize(size, force) {
      if (this._ignore_resize)
         return false;

      if (!this.iscan && this.has_canvas) return false;

      const sync_promise = this.syncDraw('canvas_resize');
      if (sync_promise === false) return false;

      if ((size === true) || (size === false)) { force = size; size = null; }

      if (isObject(size) && size.force) force = true;

      if (!force) force = this.needRedrawByResize();

      let changed = false;
      const redrawNext = indx => {
         if (!changed || (indx >= this.painters.length)) {
            this.confirmDraw();
            return changed;
         }

         return getPromise(this.painters[indx].redraw(force ? 'redraw' : 'resize')).then(() => redrawNext(indx+1));
      };

      // return sync_promise.then(() => this.ensureBrowserSize(this.pad?.fCw, this.pad?.fCh)).then(() => {

      return sync_promise.then(() => {
         changed = this.createCanvasSvg(force ? 2 : 1, size);

         if (changed && this.iscan && this.pad && this.online_canvas && !this.embed_canvas && !this.isBatchMode()) {
            if (this._resize_tmout)
               clearTimeout(this._resize_tmout);
            this._resize_tmout = setTimeout(() => {
               delete this._resize_tmout;
               if (isFunc(this.sendResized))
                  this.sendResized();
            }, 1000); // long enough delay to prevent multiple occurrence
         }

         // if canvas changed, redraw all its subitems.
         // If redrawing was forced for canvas, same applied for sub-elements
         return redrawNext(0);
      });
   }

   /** @summary Update TPad object */
   updateObject(obj) {
      if (!obj) return false;

      this.pad.fBits = obj.fBits;
      this.pad.fTitle = obj.fTitle;

      this.pad.fGridx = obj.fGridx;
      this.pad.fGridy = obj.fGridy;
      this.pad.fTickx = obj.fTickx;
      this.pad.fTicky = obj.fTicky;
      this.pad.fLogx = obj.fLogx;
      this.pad.fLogy = obj.fLogy;
      this.pad.fLogz = obj.fLogz;

      this.pad.fUxmin = obj.fUxmin;
      this.pad.fUxmax = obj.fUxmax;
      this.pad.fUymin = obj.fUymin;
      this.pad.fUymax = obj.fUymax;

      this.pad.fX1 = obj.fX1;
      this.pad.fX2 = obj.fX2;
      this.pad.fY1 = obj.fY1;
      this.pad.fY2 = obj.fY2;

      this.pad.fLeftMargin = obj.fLeftMargin;
      this.pad.fRightMargin = obj.fRightMargin;
      this.pad.fBottomMargin = obj.fBottomMargin;
      this.pad.fTopMargin = obj.fTopMargin;

      this.pad.fFillColor = obj.fFillColor;
      this.pad.fFillStyle = obj.fFillStyle;
      this.pad.fLineColor = obj.fLineColor;
      this.pad.fLineStyle = obj.fLineStyle;
      this.pad.fLineWidth = obj.fLineWidth;

      this.pad.fPhi = obj.fPhi;
      this.pad.fTheta = obj.fTheta;
      this.pad.fEditable = obj.fEditable;

      if (this.iscan)
         this.checkSpecialsInPrimitives(obj);

      const fp = this.getFramePainter();
      if (fp) fp.updateAttributes(!fp.$modifiedNDC);

      if (!obj.fPrimitives) return false;

      let isany = false, p = 0;
      for (let n = 0; n < obj.fPrimitives.arr?.length; ++n) {
         if (obj.fPrimitives.arr[n].$special)
            continue;
         while (p < this.painters.length) {
            const op = this.painters[p++];
            if (!op._primitive) continue;
            if (op.updateObject(obj.fPrimitives.arr[n], obj.fPrimitives.opt[n]))
               isany = true;
            break;
         }
      }

      return isany;
   }

   /** @summary add legend object to the pad and redraw it
     * @private */
   async buildLegend(x1, y1, x2, y2, title, opt) {
      const lp = this.findPainterFor(null, '', clTLegend);

      if (!lp && !isFunc(this.drawObject))
         return Promise.reject(Error('Not possible to build legend while module draw.mjs was not load'));

      const leg = lp?.getObject() ?? create(clTLegend),
            pad = this.getRootPad(true);

      leg.fPrimitives.Clear();

      for (let k = 0; k < this.painters.length; ++k) {
         const painter = this.painters[k],
               obj = painter.getObject();
         if (!obj || obj.fName === kTitle || obj.fName === 'stats' || painter.draw_content === false ||
              obj._typename === clTLegend || obj._typename === clTHStack || obj._typename === clTMultiGraph)
            continue;

         const entry = create(clTLegendEntry);
         entry.fObject = obj;
         entry.fLabel = painter.getItemName();
         if ((opt === 'all') || !entry.fLabel)
             entry.fLabel = obj.fName;
         entry.fOption = '';
         if (!entry.fLabel) continue;

         if (painter.lineatt?.used)
            entry.fOption += 'l';
         if (painter.fillatt?.used)
            entry.fOption += 'f';
         if (painter.markeratt?.used)
            entry.fOption += 'p';
         if (!entry.fOption)
            entry.fOption = 'l';

         leg.fPrimitives.Add(entry);
      }

      if (lp)
         return lp.redraw();

      const szx = 0.4;
      let szy = leg.fPrimitives.arr.length;
      // no entries - no need to draw legend
      if (!szy) return null;
      if (szy > 8) szy = 8;
      szy *= 0.1;

      if ((x1 === x2) || (y1 === y2)) {
         leg.fX1NDC = szx * pad.fLeftMargin + (1 - szx) * (1 - pad.fRightMargin);
         leg.fY1NDC = (1 - szy) * (1 - pad.fTopMargin) + szy * pad.fBottomMargin;
         leg.fX2NDC = 0.99 - pad.fRightMargin;
         leg.fY2NDC = 0.99 - pad.fTopMargin;
         if (opt === undefined) opt = 'autoplace';
      } else {
         leg.fX1NDC = x1;
         leg.fY1NDC = y1;
         leg.fX2NDC = x2;
         leg.fY2NDC = y2;
      }
      leg.fFillStyle = 1001;
      leg.fTitle = title ?? '';

      return this.drawObject(this, leg, opt);
   }

   /** @summary Add object painter to list of primitives
     * @private */
   addObjectPainter(objpainter, lst, indx) {
      if (objpainter && lst && lst[indx] && (objpainter.snapid === undefined)) {
         // keep snap id in painter, will be used for the
         if (this.painters.indexOf(objpainter) < 0)
            this.painters.push(objpainter);

         objpainter.snapid = lst[indx].fObjectID;
         const setSubSnaps = p => {
            if (!p._unique_painter_id) return;
            for (let k = 0; k < this.painters.length; ++k) {
               const sub = this.painters[k];
               if ((sub._main_painter_id === p._unique_painter_id) && sub._secondary_id) {
                  sub.snapid = p.snapid + '#' + sub._secondary_id;
                  setSubSnaps(sub);
               }
            }
         };

         setSubSnaps(objpainter);
      }
   }

   /** @summary Process snap with style
     * @private */
   processSnapStyle(snap) {
      Object.assign(gStyle, snap.fSnapshot);
   }

   /** @summary Process snap with colors
     * @private */
   processSnapColors(snap) {
      const ListOfColors = decodeWebCanvasColors(snap.fSnapshot.fOper);

      // set global list of colors
      if (!this.options || this.options.GlobalColors)
         adoptRootColors(ListOfColors);

      const greyscale = this.pad?.TestBit(kIsGrayscale) ?? false,
            colors = extendRootColors(null, ListOfColors, greyscale);

      // copy existing colors and extend with new values
      this._custom_colors = this.options?.LocalColors ? colors : null;

      // set palette
      if (snap.fSnapshot.fBuf && (!this.options || !this.options.IgnorePalette)) {
         const indexes = [], palette = [];
         for (let n = 0; n < snap.fSnapshot.fBuf.length; ++n) {
            indexes[n] = Math.round(snap.fSnapshot.fBuf[n]);
            palette[n] = colors[indexes[n]];
         }
         this._custom_palette_indexes = indexes;
         this._custom_palette_colors = palette;
         this.custom_palette = new ColorPalette(palette, greyscale);
      } else {
         delete this._custom_palette_indexes;
         delete this._custom_palette_colors;
         delete this.custom_palette;
      }
   }

   /** @summary Process snap with custom font
     * @private */
   processSnapFont(snap) {
      const arr = snap.fSnapshot.fOper.split(':');
      addCustomFont(Number.parseInt(arr[0]), arr[1], arr[2], arr[3]);
   }

   /** @summary Process special snaps like colors or style objects
     * @return {Promise} index where processing should start
     * @private */
   processSpecialSnaps(lst) {
      while (lst?.length) {
         const snap = lst[0];

         // gStyle object
         if (snap.fKind === webSnapIds.kStyle) {
            lst.shift();
            this.processSnapStyle(snap);
         } else if (snap.fKind === webSnapIds.kColors) {
            lst.shift();
            this.processSnapColors(snap);
         } else if (snap.fKind === webSnapIds.kFont) {
            lst.shift();
            this.processSnapFont(snap);
         } else
            break;
      }
   }

   /** @summary Function called when drawing next snapshot from the list
     * @return {Promise} for drawing of the snap
     * @private */
   async drawNextSnap(lst, indx) {
      if (indx === undefined) {
         indx = -1;
         this._snaps_map = {}; // to control how much snaps are drawn
         this._num_primitives = lst ? lst.length : 0;
      }

      ++indx; // change to the next snap

      if (!lst || (indx >= lst.length)) {
         delete this._snaps_map;
         return this;
      }

      const snap = lst[indx];

      // gStyle object
      if (snap.fKind === webSnapIds.kStyle) {
         this.processSnapStyle(snap);
         return this.drawNextSnap(lst, indx); // call next
      }

      // list of colors
      if (snap.fKind === webSnapIds.kColors) {
         this.processSnapColors(snap);
         return this.drawNextSnap(lst, indx); // call next
      }

      const snapid = snap.fObjectID,
            is_frame = (snap.fKind === webSnapIds.kObject) && (snap.fSnapshot?._typename === clTFrame);
      let cnt = (this._snaps_map[snapid] || 0) + 1,
          objpainter = null;

      this._snaps_map[snapid] = cnt; // check how many objects with same snapid drawn, use them again

      // first appropriate painter for the object
      // if same object drawn twice, two painters will exists
      for (let k = 0; k < this.painters.length; ++k) {
         const subp = this.painters[k];
         if (subp.snapid === snapid) {
            if (--cnt === 0) {
               objpainter = subp;
               break;
            }
         } else if (is_frame && !subp.snapid && (subp === this.getFramePainter())) {
            // workaround for the case when frame created afterwards by server
            subp.snapid = snapid;
            objpainter = subp;
            break;
         }
      }

      if (objpainter) {
         if (snap.fKind === webSnapIds.kSubPad) // sub-pad
            return objpainter.redrawPadSnap(snap).then(() => this.drawNextSnap(lst, indx));

         let promise;

         if (snap.fKind === webSnapIds.kObject) { // object itself
            if (objpainter.updateObject(snap.fSnapshot, snap.fOption, true))
               promise = objpainter.redraw();
         } else if (snap.fKind === webSnapIds.kSVG) { // update SVG
            if (objpainter.updateObject(snap.fSnapshot))
               promise = objpainter.redraw();
         }

         return getPromise(promise).then(() => this.drawNextSnap(lst, indx)); // call next
      }

      if (snap.fKind === webSnapIds.kSubPad) { // sub-pad
         const subpad = snap.fSnapshot;

         subpad.fPrimitives = null; // clear primitives, they just because of I/O

         const padpainter = new TPadPainter(this, subpad, false);
         padpainter.decodeOptions(snap.fOption);
         padpainter.addToPadPrimitives();
         padpainter.snapid = snap.fObjectID;
         padpainter.is_active_pad = !!snap.fActive; // enforce boolean flag
         padpainter._readonly = snap.fReadOnly ?? false; // readonly flag
         padpainter._snap_primitives = snap.fPrimitives; // keep list to be able find primitive
         padpainter._has_execs = snap.fHasExecs ?? false; // are there pad execs, enables some interactive features

         if (subpad.$disable_drawing)
            padpainter.pad_draw_disabled = true;

         padpainter.processSpecialSnaps(snap.fPrimitives); // need to process style and colors before creating graph elements

         padpainter.createPadSvg();

         if (padpainter.matchObjectType(clTPad) && (snap.fPrimitives.length > 0))
            padpainter.addPadButtons(true);

         // we select current pad, where all drawing is performed
         return padpainter.drawNextSnap(snap.fPrimitives).then(() => {
            padpainter.addPadInteractive();
            return this.drawNextSnap(lst, indx); // call next
         });
      }

      // here the case of normal drawing, will be handled in promise
      if (((snap.fKind === webSnapIds.kObject) || (snap.fKind === webSnapIds.kSVG)) && (snap.fOption !== '__ignore_drawing__')) {
         return this.drawObject(this, snap.fSnapshot, snap.fOption).then(objpainter => {
            this.addObjectPainter(objpainter, lst, indx);
            return this.drawNextSnap(lst, indx);
         });
      }

      return this.drawNextSnap(lst, indx);
   }

   /** @summary Return painter with specified id
     * @private */
   findSnap(snapid) {
      if (this.snapid === snapid)
         return this;

      if (!this.painters)
         return null;

      for (let k = 0; k < this.painters.length; ++k) {
         let sub = this.painters[k];

         if (isFunc(sub.findSnap))
            sub = sub.findSnap(snapid);
         else if (sub.snapid !== snapid)
            sub = null;

         if (sub) return sub;
      }

      return null;
   }

   /** @summary Redraw pad snap
     * @desc Online version of drawing pad primitives
     * for the canvas snapshot contains list of objects
     * as first entry, graphical properties of canvas itself is provided
     * in ROOT6 it also includes primitives, but we ignore them
     * @return {Promise} with pad painter when drawing completed
     * @private */
   async redrawPadSnap(snap) {
      if (!snap?.fPrimitives)
         return this;

      this.is_active_pad = !!snap.fActive; // enforce boolean flag
      this._readonly = snap.fReadOnly ?? false; // readonly flag
      this._snap_primitives = snap.fPrimitives; // keep list to be able find primitive
      this._has_execs = snap.fHasExecs ?? false; // are there pad execs, enables some interactive features

      const first = snap.fSnapshot;
      first.fPrimitives = null; // primitives are not interesting, they are disabled in IO

      // if there are execs in the pad, deliver events to the server
      this._deliver_webcanvas_events = first.fExecs?.arr?.length > 0;

      if (this.snapid === undefined) {
         // first time getting snap, create all gui elements first

         this.snapid = snap.fObjectID;

         this.draw_object = this.pad = first; // first object is pad

         // this._fixed_size = true;

         // if canvas size not specified in batch mode, temporary use 900x700 size
         if (this.isBatchMode() && (!first.fCw || !first.fCh)) { first.fCw = 900; first.fCh = 700; }

         // case of ROOT7 with always dummy TPad as first entry
         if (!first.fCw || !first.fCh) this._fixed_size = false;

         const mainid = this.selectDom().attr('id');

         if (!this.isBatchMode() && !this.use_openui && !this.brlayout && mainid && isStr(mainid)) {
            this.brlayout = new BrowserLayout(mainid, null, this);
            this.brlayout.create(mainid, true);
            this.setDom(this.brlayout.drawing_divid()); // need to create canvas
            registerForResize(this.brlayout);
         }

         this.processSpecialSnaps(snap.fPrimitives);

         this.createCanvasSvg(0);

         if (!this.isBatchMode())
            this.addPadButtons(true);

         if (typeof snap.fHighlightConnect !== 'undefined')
            this._highlight_connect = snap.fHighlightConnect;

         let pr = Promise.resolve(true);

         if (isStr(snap.fScripts) && snap.fScripts) {
            let src = '', m = null;

            if (snap.fScripts.indexOf('modules:') === 0)
               m = snap.fScripts.slice(8).split(';');
            else if (snap.fScripts.indexOf('load:') === 0)
               src = snap.fScripts.slice(5).split(';');
            else if (snap.fScripts.indexOf('assert:') === 0)
               src = snap.fScripts.slice(7);

            pr = (m !== null) ? loadModules(m) : (src ? loadScript(src) : injectCode(snap.fScripts));
         }

         return pr.then(() => this.drawNextSnap(snap.fPrimitives)).then(() => {
            if (isFunc(this.onCanvasUpdated))
               this.onCanvasUpdated(this);
            return this;
         });
      }

      this.updateObject(first); // update only object attributes

      // apply all changes in the object (pad or canvas)
      if (this.iscan)
         this.createCanvasSvg(2);
       else
         this.createPadSvg(true);

      const matchPrimitive = (painters, primitives, class_name, obj_name) => {
         const painter = painters.find(p => {
            if (p.snapid === undefined) return false;
            if (!p.matchObjectType(class_name)) return false;
            if (obj_name && (!p.getObject() || (p.getObject().fName !== obj_name))) return false;
            return true;
         });
         if (!painter) return;
         const primitive = primitives.find(pr => {
            if ((pr.fKind !== 1) || !pr.fSnapshot || (pr.fSnapshot._typename !== class_name)) return false;
            if (obj_name && (pr.fSnapshot.fName !== obj_name)) return false;
            return true;
         });
         if (!primitive) return;

         // force painter to use new object id
         if (painter.snapid !== primitive.fObjectID)
            painter.snapid = primitive.fObjectID;
      };

      // check if frame or title was recreated, we could reassign handlers for them directly
      // while this is temporary objects, which can be recreated very often, try to catch such situation ourself
      if (!snap.fWithoutPrimitives) {
         matchPrimitive(this.painters, snap.fPrimitives, clTFrame);
         matchPrimitive(this.painters, snap.fPrimitives, clTPaveText, kTitle);
      }

      let isanyfound = false, isanyremove = false;

      // find and remove painters which no longer exists in the list
      if (!snap.fWithoutPrimitives) {
         for (let k = 0; k < this.painters.length; ++k) {
            const sub = this.painters[k];

            // skip secondary painters or painters without snapid
            if (!isStr(sub.snapid) || sub.isSecondary()) continue; // look only for painters with snapid

            const prim = snap.fPrimitives.find(prim => (prim.fObjectID === sub.snapid && !prim.$checked));
            if (prim) {
               isanyfound = true;
               prim.$checked = true;
            } else {
               // remove painter which does not found in the list of snaps
               k = this.removePrimitive(k); // index modified
               isanyremove = true;
               if (k === -111) {
                  // main painter is removed - do full cleanup and redraw
                  isanyfound = false;
                  break;
               }
            }
         }
      }

      if (isanyremove)
         delete this.pads_cache;

      if (!isanyfound && !snap.fWithoutPrimitives) {
         // TODO: maybe just remove frame painter?
         const fp = this.getFramePainter(),
               old_painters = this.painters;
         this.painters = [];
         old_painters.forEach(objp => {
            if (fp !== objp) objp.cleanup();
         });
         delete this.main_painter_ref;
         if (fp) {
            this.painters.push(fp);
            fp.cleanFrameDrawings();
            fp.redraw();
         }
         if (isFunc(this.removePadButtons)) this.removePadButtons();
         this.addPadButtons(true);
      }

      return this.drawNextSnap(snap.fPrimitives).then(() => {
         this.addPadInteractive();
         if (getActivePad() === this)
            this.getCanvPainter()?.producePadEvent('padredraw', this);
         if (isFunc(this.onCanvasUpdated))
            this.onCanvasUpdated(this);
         return this;
      });
   }

   /** @summary Deliver mouse move or click event to the web canvas
     * @private */
   deliverWebCanvasEvent(kind, x, y, hints) {
      if (!this._deliver_webcanvas_events || !this.is_active_pad || this.doingDraw() || x === undefined || y === undefined) return;
      const cp = this.getCanvPainter();
      if (!cp || !cp._websocket || !cp._websocket.canSend(2) || cp._readonly) return;

      let selobj_snapid = '';
      if (hints && hints[0] && hints[0].painter?.snapid)
         selobj_snapid = hints[0].painter.snapid.toString();

      const msg = JSON.stringify([this.snapid, kind, x.toString(), y.toString(), selobj_snapid]);

      cp.sendWebsocket(`EVENT:${msg}`);
   }

   /** @summary Create image for the pad
     * @desc Used with web-based canvas to create images for server side
     * @return {Promise} with image data, coded with btoa() function
     * @private */
   async createImage(format) {
      if ((format === 'png') || (format === 'jpeg') || (format === 'svg') || (format === 'webp') || (format === 'pdf')) {
         return this.produceImage(true, format).then(res => {
            if (!res || (format === 'svg')) return res;
            const separ = res.indexOf('base64,');
            return (separ > 0) ? res.slice(separ+7) : '';
         });
      }

      return '';
   }

   /** @summary Collects pad information for TWebCanvas
     * @desc need to update different states
     * @private */
   getWebPadOptions(arg, cp) {
      let is_top = (arg === undefined), elem = null, scan_subpads = true;
      // no any options need to be collected in readonly mode
      if (is_top && this._readonly)
         return '';
      if (arg === 'only_this') {
         is_top = true;
         scan_subpads = false;
      } else if (arg === 'with_subpads') {
         is_top = true;
         scan_subpads = true;
      }
      if (is_top) arg = [];
      if (!cp) cp = this.iscan ? this : this.getCanvPainter();

      if (this.snapid) {
         elem = { _typename: 'TWebPadOptions', snapid: this.snapid.toString(),
                  active: !!this.is_active_pad,
                  cw: 0, ch: 0, w: [],
                  bits: 0, primitives: [],
                  logx: this.pad.fLogx, logy: this.pad.fLogy, logz: this.pad.fLogz,
                  gridx: this.pad.fGridx, gridy: this.pad.fGridy,
                  tickx: this.pad.fTickx, ticky: this.pad.fTicky,
                  mleft: this.pad.fLeftMargin, mright: this.pad.fRightMargin,
                  mtop: this.pad.fTopMargin, mbottom: this.pad.fBottomMargin,
                  xlow: 0, ylow: 0, xup: 1, yup: 1,
                  zx1: 0, zx2: 0, zy1: 0, zy2: 0, zz1: 0, zz2: 0, phi: 0, theta: 0 };

         if (this.iscan) {
            elem.bits = this.getStatusBits();
            elem.cw = this.getPadWidth();
            elem.ch = this.getPadHeight();
            elem.w = [window.screenLeft, window.screenTop, window.outerWidth, window.outerHeight];
         } else if (cp) {
            const cw = cp.getPadWidth(), ch = cp.getPadHeight(), rect = this.getPadRect();
            elem.cw = cw;
            elem.ch = ch;
            elem.xlow = rect.x / cw;
            elem.ylow = 1 - (rect.y + rect.height) / ch;
            elem.xup = elem.xlow + rect.width / cw;
            elem.yup = elem.ylow + rect.height / ch;
         }

         if ((this.pad.fTheta !== 30) || (this.pad.fPhi !== 30)) {
            elem.phi = this.pad.fPhi;
            elem.theta = this.pad.fTheta;
         }

         if (this.getPadRanges(elem))
            arg.push(elem);
         else
            console.log(`fail to get ranges for pad ${this.pad.fName}`);
      }

      this.painters.forEach(sub => {
         if (isFunc(sub.getWebPadOptions)) {
            if (scan_subpads) sub.getWebPadOptions(arg, cp);
         } else {
            const opt = createWebObjectOptions(sub);
            if (opt)
               elem.primitives.push(opt);
         }
      });

      if (is_top) return toJSON(arg);
   }

   /** @summary returns actual ranges in the pad, which can be applied to the server
     * @private */
   getPadRanges(r) {
      if (!r) return false;

      const main = this.getFramePainter(),
            p = this.svg_this_pad();

      r.ranges = main?.ranges_set ?? false; // indicate that ranges are assigned

      r.ux1 = r.px1 = r.ranges ? main.scale_xmin : 0; // need to initialize for JSON reader
      r.uy1 = r.py1 = r.ranges ? main.scale_ymin : 0;
      r.ux2 = r.px2 = r.ranges ? main.scale_xmax : 0;
      r.uy2 = r.py2 = r.ranges ? main.scale_ymax : 0;
      r.uz1 = r.ranges ? (main.scale_zmin ?? 0) : 0;
      r.uz2 = r.ranges ? (main.scale_zmax ?? 0) : 0;

      if (main) {
         if (main.zoom_xmin !== main.zoom_xmax) {
            r.zx1 = main.zoom_xmin; r.zx2 = main.zoom_xmax;
         }

         if (main.zoom_ymin !== main.zoom_ymax) {
            r.zy1 = main.zoom_ymin; r.zy2 = main.zoom_ymax;
         }

         if (main.zoom_zmin !== main.zoom_zmax) {
            r.zz1 = main.zoom_zmin; r.zz2 = main.zoom_zmax;
         }
      }

      if (!r.ranges || p.empty()) return true;

      // calculate user range for full pad
      const func = (log, value, err) => {
         if (!log) return value;
         if (value <= 0) return err;
         value = Math.log10(value);
         if (log > 1) value = value/Math.log10(log);
         return value;
      }, frect = main.getFrameRect();

      r.ux1 = func(main.logx, r.ux1, 0);
      r.ux2 = func(main.logx, r.ux2, 1);

      let k = (r.ux2 - r.ux1)/(frect.width || 10);
      r.px1 = r.ux1 - k*frect.x;
      r.px2 = r.px1 + k*this.getPadWidth();

      r.uy1 = func(main.logy, r.uy1, 0);
      r.uy2 = func(main.logy, r.uy2, 1);

      k = (r.uy2 - r.uy1)/(frect.height || 10);
      r.py1 = r.uy1 - k*frect.y;
      r.py2 = r.py1 + k*this.getPadHeight();

      return true;
   }

   /** @summary Show context menu for specified item
     * @private */
   itemContextMenu(name) {
       const rrr = this.svg_this_pad().node().getBoundingClientRect(),
             evnt = { clientX: rrr.left + 10, clientY: rrr.top + 10 };

       // use timeout to avoid conflict with mouse click and automatic menu close
       if (name === 'pad')
          return postponePromise(() => this.padContextMenu(evnt), 50);

       let selp = null, selkind;

       switch (name) {
          case 'xaxis':
          case 'yaxis':
          case 'zaxis':
             selp = this.getFramePainter();
             selkind = name[0];
             break;
          case 'frame':
             selp = this.getFramePainter();
             break;
          default: {
             const indx = parseInt(name);
             if (Number.isInteger(indx))
                selp = this.painters[indx];
          }
       }

       if (!isFunc(selp?.fillContextMenu)) return;

       return createMenu(evnt, selp).then(menu => {
          const offline_menu = selp.fillContextMenu(menu, selkind);
          if (offline_menu || selp.snapid)
             return selp.fillObjectExecMenu(menu, selkind).then(() => postponePromise(() => menu.show(), 50));
       });
   }

   /** @summary Save pad as image
     * @param {string} kind - format of saved image like 'png', 'svg' or 'jpeg'
     * @param {boolean} full_canvas - does complete canvas (true) or only frame area (false) should be saved
     * @param {string} [filename] - name of the file which should be stored
     * @desc Normally used from context menu
     * @example
     * import { getElementCanvPainter } from 'https://root.cern/js/latest/modules/base/ObjectPainter.mjs';
     * let canvas_painter = getElementCanvPainter('drawing_div_id');
     * canvas_painter.saveAs('png', true, 'canvas.png'); */
   saveAs(kind, full_canvas, filename) {
      if (!filename)
         filename = (this.this_pad_name || (this.iscan ? 'canvas' : 'pad')) + '.' + kind;

      this.produceImage(full_canvas, kind).then(imgdata => {
         if (!imgdata)
            return console.error(`Fail to produce image ${filename}`);

         if ((browser.qt5 || browser.qt6 || browser.cef3) && this.snapid) {
            console.warn(`sending file ${filename} to server`);
            let res = imgdata;
            if (kind !== 'svg') {
               const separ = res.indexOf('base64,');
               res = (separ > 0) ? res.slice(separ+7) : '';
            }
            if (res)
              this.getCanvPainter()?.sendWebsocket(`SAVE:${filename}:${res}`);
         } else {
            const prefix = (kind === 'svg') ? prSVG : (kind === 'json' ? prJSON : '');
            saveFile(filename, prefix ? prefix + encodeURIComponent(imgdata) : imgdata);
         }
      });
   }

   /** @summary Search active pad
     * @return {Object} pad painter for active pad */
   findActivePad() {
      let active_pp;
      this.forEachPainterInPad(pp => {
         if (pp.is_active_pad && !active_pp)
            active_pp = pp;
      }, 'pads');
      return active_pp;
   }

   /** @summary Produce image for the pad
     * @return {Promise} with created image */
   async produceImage(full_canvas, file_format, args) {
      if (file_format === 'json')
         return isFunc(this.produceJSON) ? this.produceJSON(full_canvas ? 2 : 0) : '';

      const use_frame = (full_canvas === 'frame'),
            elem = use_frame ? this.getFrameSvg(this.this_pad_name) : (full_canvas ? this.getCanvSvg() : this.svg_this_pad()),
            painter = (full_canvas && !use_frame) ? this.getCanvPainter() : this,
            items = []; // keep list of replaced elements, which should be moved back at the end

      if (elem.empty())
         return '';

      if (use_frame || !full_canvas) {
         const defs = this.getCanvSvg().selectChild('.canvas_defs');
         if (!defs.empty()) {
            items.push({ prnt: this.getCanvSvg(), defs });
            elem.node().insertBefore(defs.node(), elem.node().firstChild);
         }
      }

      let active_pp = null;
      painter.forEachPainterInPad(pp => {
         if (pp.is_active_pad && !active_pp) {
            active_pp = pp;
            active_pp.drawActiveBorder(null, false);
         }

         if (use_frame) return; // do not make transformations for the frame

         const item = { prnt: pp.svg_this_pad() };
         items.push(item);

         // remove buttons from each sub-pad
         const btns = pp.getLayerSvg('btns_layer', pp.this_pad_name);
         item.btns_node = btns.node();
         if (item.btns_node) {
            item.btns_prnt = item.btns_node.parentNode;
            item.btns_next = item.btns_node.nextSibling;
            btns.remove();
         }

         const fp = pp.getFramePainter();
         if (!isFunc(fp?.access3dKind)) return;

         const can3d = fp.access3dKind();
         if ((can3d !== constants.Embed3D.Overlay) && (can3d !== constants.Embed3D.Embed)) return;

         let main, canvas;
         if (isFunc(fp.render3D)) {
            main = fp;
            canvas = fp.renderer?.domElement;
         } else {
            main = fp.getMainPainter();
            canvas = main?._renderer?.domElement;
         }
         if (!isFunc(main?.render3D) || !isObject(canvas)) return;

         const sz2 = fp.getSizeFor3d(constants.Embed3D.Embed); // get size and position of DOM element as it will be embed
         main.render3D(0); // WebGL clears buffers, therefore we should render scene and convert immediately
         const dataUrl = canvas.toDataURL('image/png');

         // remove 3D drawings
         if (can3d === constants.Embed3D.Embed) {
            item.foreign = item.prnt.select('.' + sz2.clname);
            item.foreign.remove();
         }

         const svg_frame = main.getFrameSvg();
         item.frame_node = svg_frame.node();
         if (item.frame_node) {
            item.frame_next = item.frame_node.nextSibling;
            svg_frame.remove();
         }

         // add svg image
         item.img = item.prnt.insert('image', '.primitives_layer')     // create image object
                        .attr('x', sz2.x)
                        .attr('y', sz2.y)
                        .attr('width', canvas.width)
                        .attr('height', canvas.height)
                        .attr('href', dataUrl);
      }, 'pads');

      let width = elem.property('draw_width'), height = elem.property('draw_height');
      if (use_frame) {
         const fp = this.getFramePainter();
         width = fp.getFrameWidth();
         height = fp.getFrameHeight();
      }

      const arg = (file_format === 'pdf')
         ? { node: elem.node(), width, height, reset_tranform: use_frame }
         : compressSVG(`<svg width="${width}" height="${height}" xmlns="${nsSVG}">${elem.node().innerHTML}</svg>`);

      return svgToImage(arg, file_format, args).then(res => {
         // reactivate border
         active_pp?.drawActiveBorder(null, true);

         for (let k = 0; k < items.length; ++k) {
            const item = items[k];

            item.img?.remove(); // delete embed image

            const prim = item.prnt.selectChild('.primitives_layer');

            if (item.foreign) // reinsert foreign object
               item.prnt.node().insertBefore(item.foreign.node(), prim.node());

            if (item.frame_node) // reinsert frame as first in list of primitives
               prim.node().insertBefore(item.frame_node, item.frame_next);

            if (item.btns_node) // reinsert buttons
               item.btns_prnt.insertBefore(item.btns_node, item.btns_next);

            if (item.defs) // reinsert defs
               item.prnt.node().insertBefore(item.defs.node(), item.prnt.node().firstChild);
         }
         return res;
      });
   }

   /** @summary Process pad button click */
   clickPadButton(funcname, evnt) {
      if (funcname === 'CanvasSnapShot')
         return this.saveAs('png', true);

      if (funcname === 'enlargePad')
         return this.enlargePad();

      if (funcname === 'PadSnapShot')
         return this.saveAs('png', false);

      if (funcname === 'PadContextMenus') {
         evnt?.preventDefault();
         evnt?.stopPropagation();
         if (closeMenu()) return;

         return createMenu(evnt, this).then(menu => {
            menu.header('Menus');

            if (this.iscan)
               menu.add('Canvas', 'pad', this.itemContextMenu);
            else
               menu.add('Pad', 'pad', this.itemContextMenu);

            if (this.getFramePainter())
               menu.add('Frame', 'frame', this.itemContextMenu);

            const main = this.getMainPainter(); // here pad painter method

            if (main) {
               menu.add('X axis', 'xaxis', this.itemContextMenu);
               menu.add('Y axis', 'yaxis', this.itemContextMenu);
               if (isFunc(main.getDimension) && (main.getDimension() > 1))
                  menu.add('Z axis', 'zaxis', this.itemContextMenu);
            }

            if (this.painters?.length) {
               menu.separator();
               const shown = [];
               this.painters.forEach((pp, indx) => {
                  const obj = pp?.getObject();
                  if (!obj || (shown.indexOf(obj) >= 0)) return;
                  let name = isFunc(pp.getClassName) ? pp.getClassName() : (obj._typename || '');
                  if (name) name += '::';
                  name += isFunc(pp.getObjectName) ? pp.getObjectName() : (obj.fName || `item${indx}`);
                  menu.add(name, indx, this.itemContextMenu);
                  shown.push(obj);
               });
            }

            menu.show();
         });
      }

      // click automatically goes to all sub-pads
      // if any painter indicates that processing completed, it returns true
      let done = false;
      const prs = [];

      for (let i = 0; i < this.painters.length; ++i) {
         const pp = this.painters[i];

         if (isFunc(pp.clickPadButton))
            prs.push(pp.clickPadButton(funcname, evnt));

         if (!done && isFunc(pp.clickButton)) {
            done = pp.clickButton(funcname);
            if (isPromise(done)) prs.push(done);
         }
      }

      return Promise.all(prs);
   }

   /** @summary Add button to the pad
     * @private */
   addPadButton(btn, tooltip, funcname, keyname) {
      if (!settings.ToolBar || this.isBatchMode()) return;

      if (!this._buttons) this._buttons = [];
      // check if there are duplications

      for (let k = 0; k < this._buttons.length; ++k)
         if (this._buttons[k].funcname === funcname) return;

      this._buttons.push({ btn, tooltip, funcname, keyname });

      const iscan = this.iscan || !this.has_canvas;
      if (!iscan && (funcname.indexOf('Pad') !== 0) && (funcname !== 'enlargePad')) {
         const cp = this.getCanvPainter();
         if (cp && (cp !== this)) cp.addPadButton(btn, tooltip, funcname);
      }
   }

   /** @summary Show pad buttons
     * @private */
   showPadButtons() {
      if (!this._buttons) return;

       PadButtonsHandler.assign(this);
       this.showPadButtons();
   }

   /** @summary Add buttons for pad or canvas
     * @private */
   addPadButtons(is_online) {
      this.addPadButton('camera', 'Create PNG', this.iscan ? 'CanvasSnapShot' : 'PadSnapShot', 'Ctrl PrintScreen');

      if (settings.ContextMenu)
         this.addPadButton('question', 'Access context menus', 'PadContextMenus');

      const add_enlarge = !this.iscan && this.has_canvas && this.hasObjectsToDraw();

      if (add_enlarge || this.enlargeMain('verify'))
         this.addPadButton('circle', 'Enlarge canvas', 'enlargePad');

      if (is_online && this.brlayout) {
         this.addPadButton('diamand', 'Toggle Ged', 'ToggleGed');
         this.addPadButton('three_circles', 'Toggle Status', 'ToggleStatus');
      }
   }

   /** @summary Decode pad draw options
     * @private */
   decodeOptions(opt) {
      const pad = this.getObject();
      if (!pad) return;

      const d = new DrawOptions(opt);

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

      Object.assign(this.options, { GlobalColors: true, LocalColors: false, CreatePalette: 0, IgnorePalette: false, RotateFrame: false, FixFrame: false });

      if (d.check('NOCOLORS') || d.check('NOCOL')) this.options.GlobalColors = this.options.LocalColors = false;
      if (d.check('LCOLORS') || d.check('LCOL')) { this.options.GlobalColors = false; this.options.LocalColors = true; }
      if (d.check('NOPALETTE') || d.check('NOPAL')) this.options.IgnorePalette = true;
      if (d.check('ROTATE')) this.options.RotateFrame = true;
      if (d.check('FIXFRAME')) this.options.FixFrame = true;
      if (d.check('FIXSIZE') && this.iscan) this._fixed_size = true;

      if (d.check('CP', true)) this.options.CreatePalette = d.partAsInt(0, 0);

      if (d.check('NOZOOMX')) this.options.NoZoomX = true;
      if (d.check('NOZOOMY')) this.options.NoZoomY = true;
      if (d.check('GRAYSCALE') && !pad.TestBit(kIsGrayscale))
          pad.InvertBit(kIsGrayscale);

      function forEach(func, p) {
         if (!p) p = pad;
         func(p);
         const arr = p.fPrimitives?.arr || [];
         for (let i = 0; i < arr.length; ++i) {
            if (arr[i]._typename === clTPad)
               forEach(func, arr[i]);
         }
      }

      if (d.check('NOMARGINS')) forEach(p => { p.fLeftMargin = p.fRightMargin = p.fBottomMargin = p.fTopMargin = 0; });
      if (d.check('WHITE')) forEach(p => { p.fFillColor = 0; });
      if (d.check('LOG2X')) forEach(p => { p.fLogx = 2; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
      if (d.check('LOGX')) forEach(p => { p.fLogx = 1; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
      if (d.check('LOG2Y')) forEach(p => { p.fLogy = 2; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
      if (d.check('LOGY')) forEach(p => { p.fLogy = 1; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
      if (d.check('LOG2Z')) forEach(p => { p.fLogz = 2; });
      if (d.check('LOGZ')) forEach(p => { p.fLogz = 1; });
      if (d.check('LOGV')) forEach(p => { p.fLogv = 1; });
      if (d.check('LOG2')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 2; });
      if (d.check('LOG')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 1; });
      if (d.check('LNX')) forEach(p => { p.fLogx = 3; p.fUxmin = 0; p.fUxmax = 1; p.fX1 = 0; p.fX2 = 1; });
      if (d.check('LNY')) forEach(p => { p.fLogy = 3; p.fUymin = 0; p.fUymax = 1; p.fY1 = 0; p.fY2 = 1; });
      if (d.check('LN')) forEach(p => { p.fLogx = p.fLogy = p.fLogz = 3; });
      if (d.check('GRIDX')) forEach(p => { p.fGridx = 1; });
      if (d.check('GRIDY')) forEach(p => { p.fGridy = 1; });
      if (d.check('GRID')) forEach(p => { p.fGridx = p.fGridy = 1; });
      if (d.check('TICKX')) forEach(p => { p.fTickx = 1; });
      if (d.check('TICKY')) forEach(p => { p.fTicky = 1; });
      if (d.check('TICKZ')) forEach(p => { p.fTickz = 1; });
      if (d.check('TICK')) forEach(p => { p.fTickx = p.fTicky = 1; });
      ['OTX', 'OTY', 'CTX', 'CTY', 'NOEX', 'NOEY', 'RX', 'RY'].forEach(name => {
         if (d.check(name)) forEach(p => { p['$' + name] = true; });
      });

      this.storeDrawOpt(opt);
   }

   /** @summary draw TPad object */
   static async draw(dom, pad, opt) {
      const painter = new TPadPainter(dom, pad, false);
      painter.decodeOptions(opt);

      if (painter.getCanvSvg().empty()) {
         // one can draw pad without canvas
         painter.has_canvas = false;
         painter.this_pad_name = '';
         painter.setTopPainter();
      } else {
         // pad painter will be registered in the parent pad
         painter.addToPadPrimitives();
      }

      if (pad?.$disable_drawing)
         painter.pad_draw_disabled = true;

      painter.createPadSvg();

      if (painter.matchObjectType(clTPad) && (!painter.has_canvas || painter.hasObjectsToDraw()))
         painter.addPadButtons();

      // set active pad
      selectActivePad({ pp: painter, active: true });

      // flag used to prevent immediate pad redraw during first draw
      return painter.drawPrimitives().then(() => {
         painter.showPadButtons();
         painter.addPadInteractive();
         return painter;
      });
   }

} // class TPadPainter

export { TPadPainter, PadButtonsHandler, clTButton, kIsGrayscale, createWebObjectOptions };